mirror of
https://github.com/TelegramMessenger/Telegram-iOS.git
synced 2026-07-05 19:28:46 +02:00
Video tests
This commit is contained in:
parent
15880b36a2
commit
7ee4337cdc
60 changed files with 7023 additions and 99 deletions
|
|
@ -27,6 +27,7 @@ swift_library(
|
|||
"//submodules/Components/BundleIconComponent:BundleIconComponent",
|
||||
"//submodules/Components/BlurredBackgroundComponent:BlurredBackgroundComponent",
|
||||
"//submodules/TelegramUI/Components/MinimizedContainer",
|
||||
"//submodules/BrowserUI/TonutilsProxy",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import TelegramUIPreferences
|
|||
import AccountContext
|
||||
import WebKit
|
||||
import AppBundle
|
||||
import TonutilsProxy
|
||||
|
||||
private final class IpfsSchemeHandler: NSObject, WKURLSchemeHandler {
|
||||
private final class PendingTask {
|
||||
|
|
@ -19,35 +20,81 @@ private final class IpfsSchemeHandler: NSObject, WKURLSchemeHandler {
|
|||
init(sourceTask: any WKURLSchemeTask) {
|
||||
self.sourceTask = sourceTask
|
||||
|
||||
var cleanUrl = sourceTask.request.url!.absoluteString
|
||||
if let range = cleanUrl.range(of: "/ipfs/") {
|
||||
cleanUrl = "ipfs://" + String(cleanUrl[range.upperBound...])
|
||||
} else if let range = cleanUrl.range(of: "/ipns/") {
|
||||
cleanUrl = "ipns://" + String(cleanUrl[range.upperBound...])
|
||||
}
|
||||
print("Load: \(cleanUrl)")
|
||||
cleanUrl = cleanUrl.replacingOccurrences(of: "ipns://", with: "ipns/")
|
||||
cleanUrl = cleanUrl.replacingOccurrences(of: "ipfs://", with: "ipfs/")
|
||||
let mappedUrl = "https://cloudflare-ipfs.com/\(cleanUrl)"
|
||||
let isCompleted = self.isCompleted
|
||||
self.urlSessionTask = URLSession.shared.dataTask(with: URLRequest(url: URL(string: mappedUrl)!), completionHandler: { data, response, error in
|
||||
if isCompleted.swap(true) {
|
||||
if sourceTask.request.url?.scheme == "ton" {
|
||||
var cleanUrl = sourceTask.request.url!.absoluteString
|
||||
if cleanUrl.hasPrefix("ton://") {
|
||||
cleanUrl = String(cleanUrl[cleanUrl.index(cleanUrl.startIndex, offsetBy: "ton://".count)...])
|
||||
}
|
||||
|
||||
if cleanUrl == "foundation.ton" {
|
||||
guard let path = getAppBundle().path(forResource: "testsite.data", ofType: nil) else {
|
||||
return
|
||||
}
|
||||
guard let data = try? Data(contentsOf: URL(fileURLWithPath: path)) else {
|
||||
return
|
||||
}
|
||||
|
||||
let response = URLResponse(url: sourceTask.request.url!, mimeType: "text/html", expectedContentLength: data.count, textEncodingName: "UTF-8")
|
||||
sourceTask.didReceive(response)
|
||||
sourceTask.didReceive(data)
|
||||
sourceTask.didFinish()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if let error {
|
||||
sourceTask.didFailWithError(error)
|
||||
} else {
|
||||
if let response {
|
||||
sourceTask.didReceive(response)
|
||||
let mappedUrl = "http://ton.x1337.dev:8080"
|
||||
let isCompleted = self.isCompleted
|
||||
var request = URLRequest(url: URL(string: mappedUrl)!)
|
||||
request.httpBody = cleanUrl.data(using: .utf8)
|
||||
self.urlSessionTask = URLSession.shared.dataTask(with: request, completionHandler: { data, response, error in
|
||||
if isCompleted.swap(true) {
|
||||
return
|
||||
}
|
||||
if let data {
|
||||
sourceTask.didReceive(data)
|
||||
|
||||
if let error {
|
||||
sourceTask.didFailWithError(error)
|
||||
} else {
|
||||
if let response {
|
||||
sourceTask.didReceive(response)
|
||||
}
|
||||
if let data {
|
||||
sourceTask.didReceive(data)
|
||||
}
|
||||
sourceTask.didFinish()
|
||||
}
|
||||
sourceTask.didFinish()
|
||||
})
|
||||
self.urlSessionTask?.resume()
|
||||
} else {
|
||||
var cleanUrl = sourceTask.request.url!.absoluteString
|
||||
if let range = cleanUrl.range(of: "/ipfs/") {
|
||||
cleanUrl = "ipfs://" + String(cleanUrl[range.upperBound...])
|
||||
} else if let range = cleanUrl.range(of: "/ipns/") {
|
||||
cleanUrl = "ipns://" + String(cleanUrl[range.upperBound...])
|
||||
}
|
||||
})
|
||||
self.urlSessionTask?.resume()
|
||||
|
||||
cleanUrl = cleanUrl.replacingOccurrences(of: "ipns://", with: "ipns/")
|
||||
cleanUrl = cleanUrl.replacingOccurrences(of: "ipfs://", with: "ipfs/")
|
||||
let mappedUrl = "https://cloudflare-ipfs.com/\(cleanUrl)"
|
||||
let isCompleted = self.isCompleted
|
||||
self.urlSessionTask = URLSession.shared.dataTask(with: URLRequest(url: URL(string: mappedUrl)!), completionHandler: { data, response, error in
|
||||
if isCompleted.swap(true) {
|
||||
return
|
||||
}
|
||||
|
||||
if let error {
|
||||
sourceTask.didFailWithError(error)
|
||||
} else {
|
||||
if let response {
|
||||
sourceTask.didReceive(response)
|
||||
}
|
||||
if let data {
|
||||
sourceTask.didReceive(data)
|
||||
}
|
||||
sourceTask.didFinish()
|
||||
}
|
||||
})
|
||||
self.urlSessionTask?.resume()
|
||||
}
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
|
|
@ -80,6 +127,40 @@ private final class IpfsSchemeHandler: NSObject, WKURLSchemeHandler {
|
|||
}
|
||||
}
|
||||
|
||||
@available (iOS 13.0, *)
|
||||
@MainActor
|
||||
private final class SharedTonProxy {
|
||||
static let shared = SharedTonProxy()
|
||||
|
||||
private(set) var params: (host: String, port: UInt16)?
|
||||
|
||||
init() {
|
||||
let port = UInt16(1234)
|
||||
Task.detached(operation: { @MainActor [weak self] in
|
||||
do {
|
||||
let parameters = try await TonutilsProxy.shared.start(port)
|
||||
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.params = (parameters.host, parameters.port)
|
||||
} catch {
|
||||
print("\(error)")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
deinit {
|
||||
Task.detached(operation: {
|
||||
do {
|
||||
try await TonutilsProxy.shared.stop()
|
||||
} catch {
|
||||
print("\(error)")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
final class BrowserWebContent: UIView, BrowserContent, UIScrollViewDelegate {
|
||||
private let webView: WKWebView
|
||||
|
||||
|
|
@ -96,8 +177,14 @@ final class BrowserWebContent: UIView, BrowserContent, UIScrollViewDelegate {
|
|||
let configuration = WKWebViewConfiguration()
|
||||
|
||||
if context.sharedContext.immediateExperimentalUISettings.browserExperiment {
|
||||
configuration.setURLSchemeHandler(IpfsSchemeHandler(), forURLScheme: "ipns")
|
||||
if #available (iOS 13.0, *) {
|
||||
if let params = SharedTonProxy.shared.params {
|
||||
configuration.setURLSchemeHandler(TonutilsURLSchemeHandler(address: params.host, port: params.port))
|
||||
}
|
||||
}
|
||||
/*configuration.setURLSchemeHandler(IpfsSchemeHandler(), forURLScheme: "ipns")
|
||||
configuration.setURLSchemeHandler(IpfsSchemeHandler(), forURLScheme: "ipfs")
|
||||
configuration.setURLSchemeHandler(IpfsSchemeHandler(), forURLScheme: "ton")*/
|
||||
}
|
||||
|
||||
self.webView = WKWebView(frame: CGRect(), configuration: configuration)
|
||||
|
|
|
|||
18
submodules/BrowserUI/TonutilsProxy/BUILD
Normal file
18
submodules/BrowserUI/TonutilsProxy/BUILD
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "TonutilsProxy",
|
||||
module_name = "TonutilsProxy",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/BrowserUI/TonutilsProxyBridge",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
//
|
||||
// Created by Adam Stragner
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
internal extension URL {
|
||||
var isTON: Bool {
|
||||
if #available(iOS 13.0, *) {
|
||||
return !TonutilsProxy.SupportedDomain
|
||||
.allCases
|
||||
.map({ ".\($0)" })
|
||||
.filter({ (host ?? "").hasSuffix($0) })
|
||||
.isEmpty
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
//
|
||||
// Created by Adam Stragner
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
internal extension URLSession {
|
||||
static let `default` = URLSession(configuration: .default)
|
||||
static var proxyable: URLSession?
|
||||
|
||||
static func proxyable(with connectionProxyDictionary: [AnyHashable: Any]) -> URLSession {
|
||||
let configuration = URLSessionConfiguration.default
|
||||
configuration.connectionProxyDictionary = connectionProxyDictionary
|
||||
return URLSession(configuration: configuration)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
//
|
||||
// Created by Adam Stragner
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import WebKit
|
||||
|
||||
internal extension WKURLSchemeTask {
|
||||
var identifier: AnyHashable {
|
||||
AnyHashable(request)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
//
|
||||
// Created by Adam Stragner
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import WebKit
|
||||
|
||||
/// `WKWebView` does not allow the addition of a custom` WKURLSchemeHandler` for the `HTTP` and `HTTPS` schemes.
|
||||
/// However, this workaround enables us to accomplish this task.
|
||||
internal extension WKWebView {
|
||||
static let _swizzle: Void = {
|
||||
let origin = class_getClassMethod(WKWebView.self, #selector(WKWebView.handlesURLScheme(_:)))
|
||||
let swizzled = class_getClassMethod(
|
||||
WKWebView.self,
|
||||
#selector(WKWebView.swizzled_handlesURLScheme(_:))
|
||||
)
|
||||
|
||||
guard let origin, let swizzled
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
method_exchangeImplementations(origin, swizzled)
|
||||
return ()
|
||||
}()
|
||||
|
||||
@objc(swizzled_handlesURLScheme:)
|
||||
private static func swizzled_handlesURLScheme(_ urlScheme: String) -> Bool {
|
||||
guard !TonutilsURLSchemeHandler.schemas.contains(urlScheme)
|
||||
else {
|
||||
return false
|
||||
}
|
||||
|
||||
return swizzled_handlesURLScheme(urlScheme)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
//
|
||||
// Created by Adam Stragner
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import WebKit
|
||||
|
||||
public extension WKWebViewConfiguration {
|
||||
func setURLSchemeHandler(_ urlSchemeHandler: TonutilsURLSchemeHandler) {
|
||||
let _ = WKWebView._swizzle
|
||||
TonutilsURLSchemeHandler.schemas.forEach({
|
||||
setURLSchemeHandler(urlSchemeHandler, forURLScheme: $0)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
//
|
||||
// Created by Adam Stragner
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import TonutilsProxyBridge
|
||||
|
||||
// MARK: - TonutilsProxy
|
||||
|
||||
@available(iOS 13.0, *)
|
||||
public final class TonutilsProxy: TPBTunnel {
|
||||
enum SupportedDomain: String, CaseIterable, RawRepresentable {
|
||||
case ton
|
||||
case adnl
|
||||
case tme = "t.me"
|
||||
case bag
|
||||
}
|
||||
|
||||
public static var shared: TonutilsProxy {
|
||||
shared()
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func start(_ port: UInt16 = 9090) async throws -> TPBTunnelParameters {
|
||||
try await start(withPort: port)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
//
|
||||
// Created by Adam Stragner
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
// MARK: - TonutilsTunnelError
|
||||
|
||||
public enum TonutilsTunnelError {
|
||||
case unableUpdateNetworkSettings(underlyingError: Error)
|
||||
}
|
||||
|
||||
// MARK: LocalizedError
|
||||
|
||||
extension TonutilsTunnelError: LocalizedError {
|
||||
public var errorDescription: String? {
|
||||
switch self {
|
||||
case let .unableUpdateNetworkSettings(underlyingError):
|
||||
return "[TonutilsTunnelError]: Unable update network settings - \(underlyingError)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
//
|
||||
// Created by Adam Stragner
|
||||
//
|
||||
|
||||
import NetworkExtension
|
||||
|
||||
@available(iOS 13.0, *)
|
||||
open class TonproxyTunnelProvider: NEPacketTunnelProvider {
|
||||
// MARK: Open
|
||||
|
||||
open var preferredPort: UInt16 {
|
||||
9090
|
||||
}
|
||||
|
||||
open override func startTunnel(options: [String: NSObject]? = nil) async throws {
|
||||
let tunnel = TonutilsProxy.shared
|
||||
let parameters = try await tunnel.start(preferredPort)
|
||||
|
||||
let settings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: parameters.host)
|
||||
|
||||
settings.mtu = NSNumber(value: 1500)
|
||||
settings.ipv4Settings = NEIPv4Settings(
|
||||
addresses: ["127.0.0.1"],
|
||||
subnetMasks: ["255.255.255.255"]
|
||||
)
|
||||
|
||||
settings.proxySettings = {
|
||||
let proxySettings = NEProxySettings()
|
||||
proxySettings.httpEnabled = true
|
||||
proxySettings.httpServer = NEProxyServer(
|
||||
address: parameters.host,
|
||||
port: Int(parameters.port)
|
||||
)
|
||||
|
||||
proxySettings.httpsEnabled = false
|
||||
proxySettings.excludeSimpleHostnames = false
|
||||
proxySettings.matchDomains = TonutilsProxy.SupportedDomain.allCases.map({
|
||||
".\($0.rawValue)"
|
||||
})
|
||||
|
||||
proxySettings.autoProxyConfigurationEnabled = true
|
||||
return proxySettings
|
||||
}()
|
||||
|
||||
do {
|
||||
try await setTunnelNetworkSettings(settings)
|
||||
} catch {
|
||||
await _stop()
|
||||
throw TonutilsTunnelError.unableUpdateNetworkSettings(underlyingError: error)
|
||||
}
|
||||
}
|
||||
|
||||
open override func stopTunnel(with reason: NEProviderStopReason) async {
|
||||
await _stop()
|
||||
}
|
||||
|
||||
open override func handleAppMessage(_ messageData: Data) async -> Data? {
|
||||
if let string = String(data: messageData, encoding: .utf8) {
|
||||
print("[TonproxyTunnelProvider]: Did handle a message - \(string)")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private func _stop() async {
|
||||
let tunnel = TonutilsProxy.shared
|
||||
do {
|
||||
try await tunnel.stop()
|
||||
} catch {
|
||||
print("\(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
//
|
||||
// Created by Adam Stragner
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import WebKit
|
||||
|
||||
// MARK: - URLSchemeHandlerSessionType
|
||||
|
||||
public enum URLSchemeHandlerSessionType {
|
||||
case `default`
|
||||
case proxy(connectionProxyDictionary: [AnyHashable: Any])
|
||||
}
|
||||
|
||||
// MARK: - TonutilsURLSchemeHandlerDelegate
|
||||
|
||||
public protocol TonutilsURLSchemeHandlerDelegate: AnyObject {
|
||||
func tonutilsURLSchemeHandler(
|
||||
_ schemeHandler: TonutilsURLSchemeHandler,
|
||||
sessionFor type: URLSchemeHandlerSessionType
|
||||
) -> URLSession
|
||||
}
|
||||
|
||||
// MARK: - TonutilsURLSchemeHandler
|
||||
|
||||
open class TonutilsURLSchemeHandler: NSObject, WKURLSchemeHandler {
|
||||
// MARK: Lifecycle
|
||||
|
||||
public init(address: String, port: UInt16) {
|
||||
self.connectionProxyDictionary = [
|
||||
kCFNetworkProxiesHTTPEnable: true,
|
||||
kCFNetworkProxiesHTTPProxy: address,
|
||||
kCFNetworkProxiesHTTPPort: port,
|
||||
]
|
||||
|
||||
super.init()
|
||||
}
|
||||
|
||||
// MARK: Open
|
||||
|
||||
open func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) {
|
||||
guard let url = urlSchemeTask.request.url
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
if url.isTON {
|
||||
route(urlSchemeTask, with: .proxy(connectionProxyDictionary: connectionProxyDictionary))
|
||||
} else {
|
||||
route(urlSchemeTask, with: .default)
|
||||
}
|
||||
}
|
||||
|
||||
open func webView(_ webView: WKWebView, stop urlSchemeTask: WKURLSchemeTask) {
|
||||
tasks.removeValue(forKey: urlSchemeTask.identifier)
|
||||
}
|
||||
|
||||
// MARK: Public
|
||||
|
||||
public weak var delegate: TonutilsURLSchemeHandlerDelegate?
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
internal static let schemas = ["http"]
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private let connectionProxyDictionary: [AnyHashable: Any]
|
||||
private var tasks: [AnyHashable: URLSessionDataTask] = [:]
|
||||
|
||||
private func route(
|
||||
_ schemeTask: WKURLSchemeTask,
|
||||
with sesstionType: URLSchemeHandlerSessionType
|
||||
) {
|
||||
let session = session(for: sesstionType)
|
||||
let task = session.dataTask(
|
||||
with: schemeTask.request,
|
||||
completionHandler: { [weak schemeTask, weak self] data, response, error in
|
||||
guard let self, let schemeTask
|
||||
else {
|
||||
schemeTask?.didFailWithError(URLError(.cancelled))
|
||||
return
|
||||
}
|
||||
|
||||
finish(schemeTask, data: data, response: response, error: error)
|
||||
}
|
||||
)
|
||||
|
||||
tasks[schemeTask.identifier] = task
|
||||
task.resume()
|
||||
}
|
||||
|
||||
private func finish(
|
||||
_ schemeTask: WKURLSchemeTask,
|
||||
data: Data?,
|
||||
response: URLResponse?,
|
||||
error: Error?
|
||||
) {
|
||||
tasks.removeValue(forKey: schemeTask.identifier)
|
||||
if let error = error, error._code != NSURLErrorCancelled {
|
||||
schemeTask.didFailWithError(error)
|
||||
} else {
|
||||
if let response = response {
|
||||
schemeTask.didReceive(response)
|
||||
}
|
||||
if let data = data {
|
||||
schemeTask.didReceive(data)
|
||||
}
|
||||
|
||||
schemeTask.didFinish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension TonutilsURLSchemeHandler {
|
||||
func session(for type: URLSchemeHandlerSessionType) -> URLSession {
|
||||
guard let session = delegate?.tonutilsURLSchemeHandler(self, sessionFor: type)
|
||||
else {
|
||||
return _session(for: type)
|
||||
}
|
||||
return session
|
||||
}
|
||||
|
||||
private func _session(for type: URLSchemeHandlerSessionType) -> URLSession {
|
||||
switch type {
|
||||
case .default:
|
||||
return .default
|
||||
case let .proxy(connectionProxyDictionary):
|
||||
if let session = URLSession.proxyable {
|
||||
return session
|
||||
} else {
|
||||
let session: URLSession = .proxyable(with: connectionProxyDictionary)
|
||||
URLSession.proxyable = session
|
||||
return session
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
22
submodules/BrowserUI/TonutilsProxyBridge/BUILD
Normal file
22
submodules/BrowserUI/TonutilsProxyBridge/BUILD
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
|
||||
objc_library(
|
||||
name = "TonutilsProxyBridge",
|
||||
enable_modules = True,
|
||||
module_name = "TonutilsProxyBridge",
|
||||
srcs = glob([
|
||||
"Sources/**/*.h",
|
||||
"Sources/**/*.m",
|
||||
]),
|
||||
hdrs = glob([
|
||||
"PublicHeaders/**/*.h",
|
||||
]),
|
||||
includes = [
|
||||
"PublicHeaders",
|
||||
],
|
||||
deps = [
|
||||
"//third-party/tonutilsproxy",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
//
|
||||
// Created by Adam Stragner
|
||||
//
|
||||
|
||||
#ifndef TPB_h
|
||||
#define TPB_h
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
// _TPB_EXPORT
|
||||
#if !defined(_TPB_EXPORT)
|
||||
# if defined(__cplusplus)
|
||||
# define _TPB_EXPORT extern "C"
|
||||
# else
|
||||
# define _TPB_EXPORT extern
|
||||
# endif
|
||||
#endif /* _TPB_EXPORT */
|
||||
|
||||
// _TPB_SWIFT_ERROR
|
||||
#if !defined(_TPB_SWIFT_ERROR)
|
||||
# if __OBJC__ && __has_attribute(swift_error)
|
||||
# define _TPB_SWIFT_ERROR __attribute__((swift_error(nonnull_error)));
|
||||
# else
|
||||
# abort();
|
||||
# endif
|
||||
#endif /* _TPB_SWIFT_ERROR */
|
||||
|
||||
// _TPB_EXTERN
|
||||
#if !defined(_TPB_EXTERN)
|
||||
# if defined(__cplusplus)
|
||||
# define _TPB_EXTERN extern "C" __attribute__((visibility ("default")))
|
||||
# else
|
||||
# define _TPB_EXTERN extern __attribute__((visibility ("default")))
|
||||
# endif
|
||||
#endif /* _TPB_EXTERN */
|
||||
|
||||
#define TPB_EXPORT _TPB_EXPORT
|
||||
#define TPB_EXTERN _TPB_EXTERN
|
||||
#define TPB_SWIFT_ERROR _TPB_SWIFT_ERROR
|
||||
#define TPB_STATIC_INLINE static inline
|
||||
|
||||
#endif /* TPB_h */
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
//
|
||||
// Created by Adam Stragner
|
||||
//
|
||||
|
||||
#import <TonProxyBridge/TPB.h>
|
||||
|
||||
@class TPBTunnelParameters;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
TPB_EXPORT NSErrorDomain const TPBErrorDomain;
|
||||
|
||||
@interface TPBTunnel : NSObject
|
||||
|
||||
@property (nonatomic, readonly, retain) TPBTunnelParameters * _Nullable parameters;
|
||||
|
||||
+ (instancetype)sharedTunnel;
|
||||
- (instancetype)init NS_UNAVAILABLE;
|
||||
|
||||
- (void)startWithPort:(UInt16)port completionBlock:(void (^ _Nullable)(TPBTunnelParameters * _Nullable parameters, NSError * _Nullable error))completionBlock;
|
||||
- (void)stopWithCompletionBlock:(void (^ _Nullable)(NSError * _Nullable error))completionBlock;
|
||||
|
||||
- (BOOL)isRunning;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
//
|
||||
// Created by Adam Stragner
|
||||
//
|
||||
|
||||
#import <TonProxyBridge/TPB.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface TPBTunnelParameters : NSObject
|
||||
|
||||
@property (nonatomic, readonly, copy) NSString *host;
|
||||
@property (nonatomic, readonly, assign) UInt16 port;
|
||||
|
||||
- (instancetype)initWithHost:(NSString *)host port:(UInt16)port;
|
||||
- (NSURL *)URL;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
//
|
||||
// Created by Adam Stragner
|
||||
//
|
||||
|
||||
#ifndef TonProxyBridge_h
|
||||
#define TonProxyBridge_h
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#import <TonProxyBridge/TPB.h>
|
||||
#import <TonProxyBridge/TPBTunnel.h>
|
||||
#import <TonProxyBridge/TPBTunnelParameters.h>
|
||||
|
||||
#endif /* TPB_h */
|
||||
105
submodules/BrowserUI/TonutilsProxyBridge/Sources/TPBTunnel.m
Normal file
105
submodules/BrowserUI/TonutilsProxyBridge/Sources/TPBTunnel.m
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
//
|
||||
// Created by Adam Stragner
|
||||
//
|
||||
|
||||
#import <TonProxyBridge/TPBTunnel.h>
|
||||
#import <TonProxyBridge/TPBTunnelParameters.h>
|
||||
|
||||
#import <tonutilsproxy/tonutilsproxy.h>
|
||||
|
||||
NSErrorDomain const TPBErrorDomain = @"TPBErrorDomain";
|
||||
|
||||
@interface TPBTunnel ()
|
||||
|
||||
@property (nonatomic, retain) TPBTunnelParameters * _Nullable _parameters;
|
||||
@property (nonatomic, retain) dispatch_queue_t queue;
|
||||
|
||||
@end
|
||||
|
||||
@implementation TPBTunnel
|
||||
|
||||
- (instancetype)init_ {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_queue = dispatch_queue_create("tonutils-proxy", DISPATCH_QUEUE_SERIAL);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
+ (instancetype)sharedTunnel {
|
||||
static dispatch_once_t onceToken;
|
||||
static TPBTunnel *sharedTunnel = nil;
|
||||
dispatch_once(&onceToken, ^{
|
||||
sharedTunnel = [[TPBTunnel alloc] init_];
|
||||
});
|
||||
return sharedTunnel;
|
||||
}
|
||||
|
||||
- (void)startWithPort:(UInt16)port completionBlock:(void (^ _Nullable)(TPBTunnelParameters * _Nullable parameters, NSError * _Nullable error))completionBlock {
|
||||
dispatch_async(_queue, ^{
|
||||
TPBTunnelParameters *parameters = self._parameters;
|
||||
if (parameters != nil) {
|
||||
if (completionBlock) {
|
||||
completionBlock(parameters, nil);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
char *result = StartProxy(port);
|
||||
NSError *error = [self errorWithResult:result];
|
||||
|
||||
if (error == nil) {
|
||||
parameters = [[TPBTunnelParameters alloc] initWithHost:@"127.0.0.1" port:port];
|
||||
completionBlock(parameters, nil);
|
||||
} else {
|
||||
completionBlock(nil, error);
|
||||
}
|
||||
|
||||
self._parameters = parameters;
|
||||
});
|
||||
}
|
||||
|
||||
- (void)stopWithCompletionBlock:(void (^ _Nullable)(NSError * _Nullable error))completionBlock {
|
||||
dispatch_async(_queue, ^{
|
||||
if (self._parameters == nil) {
|
||||
if (completionBlock) {
|
||||
completionBlock(nil);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
char *result = StopProxy();
|
||||
self._parameters = nil;
|
||||
|
||||
if (completionBlock) {
|
||||
completionBlock([self errorWithResult:result]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
- (NSError * _Nullable)errorWithResult:(char *)result {
|
||||
NSString *string = [NSString stringWithUTF8String:result];
|
||||
if ([string isEqualToString:@"OK"]) {
|
||||
return nil;
|
||||
} else {
|
||||
return [[NSError alloc] initWithDomain:TPBErrorDomain code:0 userInfo:@{
|
||||
NSLocalizedDescriptionKey : [string copy]
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma Setters & Getters
|
||||
|
||||
- (TPBTunnelParameters * _Nullable)parameters {
|
||||
__block TPBTunnelParameters *parameters = nil;
|
||||
dispatch_sync(_queue, ^{
|
||||
parameters = self._parameters;
|
||||
});
|
||||
return parameters;
|
||||
}
|
||||
|
||||
- (BOOL)isRunning {
|
||||
return [self parameters] != nil;
|
||||
}
|
||||
|
||||
@end
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
//
|
||||
// Created by Adam Stragner
|
||||
//
|
||||
|
||||
#import <TonProxyBridge/TPBTunnelParameters.h>
|
||||
|
||||
@implementation TPBTunnelParameters
|
||||
|
||||
- (instancetype)initWithHost:(NSString *)host port:(UInt16)port {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_host = [host copy];
|
||||
_port = port;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (NSURL *)URL {
|
||||
NSString *path = [NSString stringWithFormat:@"%@:%d", self.host, self.port];
|
||||
return [[NSURL alloc] initWithString:path];
|
||||
}
|
||||
|
||||
@end
|
||||
|
|
@ -10,4 +10,5 @@
|
|||
#import <FFMpegBinding/FFMpegPacket.h>
|
||||
#import <FFMpegBinding/FFMpegAVCodec.h>
|
||||
#import <FFMpegBinding/FFMpegRemuxer.h>
|
||||
#import <FFMpegBinding/FFMpegLiveMuxer.h>
|
||||
#import <FFMpegBinding/FrameConverter.h>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface FFMpegLiveMuxer : NSObject
|
||||
|
||||
+ (bool)remux:(NSString * _Nonnull)path to:(NSString * _Nonnull)outPath offsetSeconds:(double)offsetSeconds;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
334
submodules/FFMpegBinding/Sources/FFMpegLiveMuxer.m
Normal file
334
submodules/FFMpegBinding/Sources/FFMpegLiveMuxer.m
Normal file
|
|
@ -0,0 +1,334 @@
|
|||
#import <FFMpegBinding/FFMpegLiveMuxer.h>
|
||||
#import <FFMpegBinding/FFMpegAVIOContext.h>
|
||||
|
||||
#include "libavutil/timestamp.h"
|
||||
#include "libavformat/avformat.h"
|
||||
#include "libavcodec/avcodec.h"
|
||||
#include "libswresample/swresample.h"
|
||||
|
||||
#define MOV_TIMESCALE 1000
|
||||
|
||||
@implementation FFMpegLiveMuxer
|
||||
|
||||
+ (bool)remux:(NSString * _Nonnull)path to:(NSString * _Nonnull)outPath offsetSeconds:(double)offsetSeconds {
|
||||
AVFormatContext *input_format_context = NULL, *output_format_context = NULL;
|
||||
AVPacket packet;
|
||||
const char *in_filename, *out_filename;
|
||||
int ret, i;
|
||||
int stream_index = 0;
|
||||
int *streams_list = NULL;
|
||||
int number_of_streams = 0;
|
||||
|
||||
struct SwrContext *swr_ctx = NULL;
|
||||
|
||||
in_filename = [path UTF8String];
|
||||
out_filename = [outPath UTF8String];
|
||||
|
||||
if ((ret = avformat_open_input(&input_format_context, in_filename, av_find_input_format("mp4"), NULL)) < 0) {
|
||||
fprintf(stderr, "Could not open input file '%s'\n", in_filename);
|
||||
goto end;
|
||||
}
|
||||
if ((ret = avformat_find_stream_info(input_format_context, NULL)) < 0) {
|
||||
fprintf(stderr, "Failed to retrieve input stream information\n");
|
||||
goto end;
|
||||
}
|
||||
|
||||
avformat_alloc_output_context2(&output_format_context, NULL, "mpegts", out_filename);
|
||||
|
||||
if (!output_format_context) {
|
||||
fprintf(stderr, "Could not create output context\n");
|
||||
ret = AVERROR_UNKNOWN;
|
||||
goto end;
|
||||
}
|
||||
|
||||
const AVCodec *aac_codec = avcodec_find_encoder(AV_CODEC_ID_AAC);
|
||||
if (!aac_codec) {
|
||||
fprintf(stderr, "Could not find AAC encoder\n");
|
||||
ret = AVERROR_UNKNOWN;
|
||||
goto end;
|
||||
}
|
||||
|
||||
AVCodecContext *aac_codec_context = avcodec_alloc_context3(aac_codec);
|
||||
if (!aac_codec_context) {
|
||||
fprintf(stderr, "Could not allocate AAC codec context\n");
|
||||
ret = AVERROR_UNKNOWN;
|
||||
goto end;
|
||||
}
|
||||
|
||||
const AVCodec *opus_decoder = avcodec_find_decoder(AV_CODEC_ID_OPUS);
|
||||
if (!opus_decoder) {
|
||||
fprintf(stderr, "Could not find Opus decoder\n");
|
||||
ret = AVERROR_UNKNOWN;
|
||||
goto end;
|
||||
}
|
||||
|
||||
AVCodecContext *opus_decoder_context = avcodec_alloc_context3(opus_decoder);
|
||||
if (!opus_decoder_context) {
|
||||
fprintf(stderr, "Could not allocate Opus decoder context\n");
|
||||
ret = AVERROR_UNKNOWN;
|
||||
goto end;
|
||||
}
|
||||
|
||||
number_of_streams = input_format_context->nb_streams;
|
||||
streams_list = av_malloc_array(number_of_streams, sizeof(*streams_list));
|
||||
|
||||
if (!streams_list) {
|
||||
ret = AVERROR(ENOMEM);
|
||||
goto end;
|
||||
}
|
||||
|
||||
for (i = 0; i < input_format_context->nb_streams; i++) {
|
||||
AVStream *out_stream;
|
||||
AVStream *in_stream = input_format_context->streams[i];
|
||||
AVCodecParameters *in_codecpar = in_stream->codecpar;
|
||||
|
||||
if (in_codecpar->codec_type != AVMEDIA_TYPE_AUDIO && in_codecpar->codec_type != AVMEDIA_TYPE_VIDEO) {
|
||||
streams_list[i] = -1;
|
||||
continue;
|
||||
}
|
||||
|
||||
streams_list[i] = stream_index++;
|
||||
|
||||
if (in_codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
|
||||
out_stream = avformat_new_stream(output_format_context, NULL);
|
||||
if (!out_stream) {
|
||||
fprintf(stderr, "Failed allocating output stream\n");
|
||||
ret = AVERROR_UNKNOWN;
|
||||
goto end;
|
||||
}
|
||||
ret = avcodec_parameters_copy(out_stream->codecpar, in_codecpar);
|
||||
if (ret < 0) {
|
||||
fprintf(stderr, "Failed to copy codec parameters\n");
|
||||
goto end;
|
||||
}
|
||||
out_stream->time_base = in_stream->time_base;
|
||||
out_stream->duration = in_stream->duration;
|
||||
} else if (in_codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
|
||||
out_stream = avformat_new_stream(output_format_context, aac_codec);
|
||||
if (!out_stream) {
|
||||
fprintf(stderr, "Failed allocating output stream\n");
|
||||
ret = AVERROR_UNKNOWN;
|
||||
goto end;
|
||||
}
|
||||
|
||||
// Set the codec parameters for the AAC encoder
|
||||
aac_codec_context->sample_rate = in_codecpar->sample_rate;
|
||||
aac_codec_context->channel_layout = in_codecpar->channel_layout ? in_codecpar->channel_layout : AV_CH_LAYOUT_STEREO;
|
||||
aac_codec_context->channels = av_get_channel_layout_nb_channels(aac_codec_context->channel_layout);
|
||||
aac_codec_context->sample_fmt = aac_codec->sample_fmts ? aac_codec->sample_fmts[0] : AV_SAMPLE_FMT_FLTP; // Use the first supported sample format
|
||||
aac_codec_context->bit_rate = 128000; // Set a default bitrate, you can adjust this as needed
|
||||
//aac_codec_context->time_base = (AVRational){1, 90000};
|
||||
|
||||
ret = avcodec_open2(aac_codec_context, aac_codec, NULL);
|
||||
if (ret < 0) {
|
||||
fprintf(stderr, "Could not open AAC encoder\n");
|
||||
goto end;
|
||||
}
|
||||
|
||||
ret = avcodec_parameters_from_context(out_stream->codecpar, aac_codec_context);
|
||||
if (ret < 0) {
|
||||
fprintf(stderr, "Failed initializing audio output stream\n");
|
||||
goto end;
|
||||
}
|
||||
|
||||
out_stream->time_base = (AVRational){1, 90000};
|
||||
out_stream->duration = av_rescale_q(in_stream->duration, in_stream->time_base, out_stream->time_base);
|
||||
|
||||
// Set up the Opus decoder context
|
||||
ret = avcodec_parameters_to_context(opus_decoder_context, in_codecpar);
|
||||
if (ret < 0) {
|
||||
fprintf(stderr, "Could not copy codec parameters to decoder context\n");
|
||||
goto end;
|
||||
}
|
||||
if (opus_decoder_context->channel_layout == 0) {
|
||||
opus_decoder_context->channel_layout = av_get_default_channel_layout(opus_decoder_context->channels);
|
||||
}
|
||||
ret = avcodec_open2(opus_decoder_context, opus_decoder, NULL);
|
||||
if (ret < 0) {
|
||||
fprintf(stderr, "Could not open Opus decoder\n");
|
||||
goto end;
|
||||
}
|
||||
|
||||
// Reset the channel layout if it was unset before opening the codec
|
||||
if (opus_decoder_context->channel_layout == 0) {
|
||||
opus_decoder_context->channel_layout = av_get_default_channel_layout(opus_decoder_context->channels);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set up the resampling context
|
||||
swr_ctx = swr_alloc_set_opts(NULL,
|
||||
aac_codec_context->channel_layout, aac_codec_context->sample_fmt, aac_codec_context->sample_rate,
|
||||
opus_decoder_context->channel_layout, opus_decoder_context->sample_fmt, opus_decoder_context->sample_rate,
|
||||
0, NULL);
|
||||
if (!swr_ctx) {
|
||||
fprintf(stderr, "Could not allocate resampler context\n");
|
||||
ret = AVERROR(ENOMEM);
|
||||
goto end;
|
||||
}
|
||||
|
||||
if ((ret = swr_init(swr_ctx)) < 0) {
|
||||
fprintf(stderr, "Failed to initialize the resampling context\n");
|
||||
goto end;
|
||||
}
|
||||
|
||||
if (!(output_format_context->oformat->flags & AVFMT_NOFILE)) {
|
||||
ret = avio_open(&output_format_context->pb, out_filename, AVIO_FLAG_WRITE);
|
||||
if (ret < 0) {
|
||||
fprintf(stderr, "Could not open output file '%s'\n", out_filename);
|
||||
goto end;
|
||||
}
|
||||
}
|
||||
|
||||
AVDictionary* opts = NULL;
|
||||
ret = avformat_write_header(output_format_context, &opts);
|
||||
if (ret < 0) {
|
||||
fprintf(stderr, "Error occurred when opening output file\n");
|
||||
goto end;
|
||||
}
|
||||
|
||||
while (1) {
|
||||
AVStream *in_stream, *out_stream;
|
||||
ret = av_read_frame(input_format_context, &packet);
|
||||
if (ret < 0)
|
||||
break;
|
||||
|
||||
in_stream = input_format_context->streams[packet.stream_index];
|
||||
if (packet.stream_index >= number_of_streams || streams_list[packet.stream_index] < 0) {
|
||||
av_packet_unref(&packet);
|
||||
continue;
|
||||
}
|
||||
|
||||
packet.stream_index = streams_list[packet.stream_index];
|
||||
out_stream = output_format_context->streams[packet.stream_index];
|
||||
|
||||
if (in_stream->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
|
||||
ret = avcodec_send_packet(opus_decoder_context, &packet);
|
||||
if (ret < 0) {
|
||||
fprintf(stderr, "Error sending packet to decoder\n");
|
||||
av_packet_unref(&packet);
|
||||
continue;
|
||||
}
|
||||
|
||||
AVFrame *frame = av_frame_alloc();
|
||||
ret = avcodec_receive_frame(opus_decoder_context, frame);
|
||||
if (ret < 0 && ret != AVERROR(EAGAIN) && ret != AVERROR_EOF) {
|
||||
fprintf(stderr, "Error receiving frame from decoder\n");
|
||||
av_frame_free(&frame);
|
||||
av_packet_unref(&packet);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ret >= 0) {
|
||||
frame->pts = frame->best_effort_timestamp;
|
||||
|
||||
AVFrame *resampled_frame = av_frame_alloc();
|
||||
resampled_frame->channel_layout = aac_codec_context->channel_layout;
|
||||
resampled_frame->sample_rate = aac_codec_context->sample_rate;
|
||||
resampled_frame->format = aac_codec_context->sample_fmt;
|
||||
resampled_frame->nb_samples = aac_codec_context->frame_size;
|
||||
|
||||
if ((ret = av_frame_get_buffer(resampled_frame, 0)) < 0) {
|
||||
fprintf(stderr, "Could not allocate resampled frame buffer\n");
|
||||
av_frame_free(&resampled_frame);
|
||||
av_frame_free(&frame);
|
||||
av_packet_unref(&packet);
|
||||
continue;
|
||||
}
|
||||
|
||||
memset(resampled_frame->data[0], 0, resampled_frame->nb_samples * 2 * 2);
|
||||
//arc4random_buf(resampled_frame->data[0], resampled_frame->nb_samples * 2 * 2);
|
||||
//memset(frame->data[0], 0, frame->nb_samples * 2 * 2);
|
||||
|
||||
if ((ret = swr_convert(swr_ctx,
|
||||
resampled_frame->data, resampled_frame->nb_samples,
|
||||
(const uint8_t **)frame->data, frame->nb_samples)) < 0) {
|
||||
fprintf(stderr, "Error while converting\n");
|
||||
av_frame_free(&resampled_frame);
|
||||
av_frame_free(&frame);
|
||||
av_packet_unref(&packet);
|
||||
continue;
|
||||
}
|
||||
|
||||
resampled_frame->pts = av_rescale_q(frame->pts, opus_decoder_context->time_base, aac_codec_context->time_base);
|
||||
|
||||
ret = avcodec_send_frame(aac_codec_context, resampled_frame);
|
||||
if (ret < 0) {
|
||||
fprintf(stderr, "Error sending frame to encoder\n");
|
||||
av_frame_free(&resampled_frame);
|
||||
av_frame_free(&frame);
|
||||
av_packet_unref(&packet);
|
||||
continue;
|
||||
}
|
||||
|
||||
AVPacket out_packet;
|
||||
av_init_packet(&out_packet);
|
||||
out_packet.data = NULL;
|
||||
out_packet.size = 0;
|
||||
|
||||
ret = avcodec_receive_packet(aac_codec_context, &out_packet);
|
||||
if (ret >= 0) {
|
||||
out_packet.pts = av_rescale_q_rnd(packet.pts, in_stream->time_base, out_stream->time_base, AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX);
|
||||
out_packet.dts = av_rescale_q_rnd(packet.dts, in_stream->time_base, out_stream->time_base, AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX);
|
||||
out_packet.pts += (int64_t)(offsetSeconds * out_stream->time_base.den);
|
||||
out_packet.dts += (int64_t)(offsetSeconds * out_stream->time_base.den);
|
||||
out_packet.duration = av_rescale_q(out_packet.duration, aac_codec_context->time_base, out_stream->time_base);
|
||||
out_packet.stream_index = packet.stream_index;
|
||||
|
||||
ret = av_interleaved_write_frame(output_format_context, &out_packet);
|
||||
if (ret < 0) {
|
||||
fprintf(stderr, "Error muxing packet\n");
|
||||
av_packet_unref(&out_packet);
|
||||
av_frame_free(&resampled_frame);
|
||||
av_frame_free(&frame);
|
||||
av_packet_unref(&packet);
|
||||
break;
|
||||
}
|
||||
av_packet_unref(&out_packet);
|
||||
}
|
||||
av_frame_free(&resampled_frame);
|
||||
av_frame_free(&frame);
|
||||
}
|
||||
} else {
|
||||
packet.pts = av_rescale_q_rnd(packet.pts, in_stream->time_base, out_stream->time_base, AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX);
|
||||
packet.dts = av_rescale_q_rnd(packet.dts, in_stream->time_base, out_stream->time_base, AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX);
|
||||
packet.pts += (int64_t)(offsetSeconds * out_stream->time_base.den);
|
||||
packet.dts += (int64_t)(offsetSeconds * out_stream->time_base.den);
|
||||
packet.duration = av_rescale_q(packet.duration, in_stream->time_base, out_stream->time_base);
|
||||
packet.pos = -1;
|
||||
|
||||
ret = av_interleaved_write_frame(output_format_context, &packet);
|
||||
if (ret < 0) {
|
||||
fprintf(stderr, "Error muxing packet\n");
|
||||
av_packet_unref(&packet);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
av_packet_unref(&packet);
|
||||
}
|
||||
|
||||
av_write_trailer(output_format_context);
|
||||
|
||||
end:
|
||||
avformat_close_input(&input_format_context);
|
||||
if (output_format_context && !(output_format_context->oformat->flags & AVFMT_NOFILE)) {
|
||||
avio_closep(&output_format_context->pb);
|
||||
}
|
||||
avformat_free_context(output_format_context);
|
||||
avcodec_free_context(&aac_codec_context);
|
||||
avcodec_free_context(&opus_decoder_context);
|
||||
av_freep(&streams_list);
|
||||
if (swr_ctx) {
|
||||
swr_free(&swr_ctx);
|
||||
}
|
||||
if (ret < 0 && ret != AVERROR_EOF) {
|
||||
fprintf(stderr, "Error occurred: %s\n", av_err2str(ret));
|
||||
return false;
|
||||
}
|
||||
|
||||
printf("Remuxed video into %s\n", outPath.UTF8String);
|
||||
return true;
|
||||
}
|
||||
|
||||
@end
|
||||
|
|
@ -244,7 +244,9 @@ public func galleryItemForEntry(
|
|||
if file.isAnimated {
|
||||
content = NativeVideoContent(id: .message(message.stableId, file.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: file), imageReference: mediaImage.flatMap({ ImageMediaReference.message(message: MessageReference(message), media: $0) }), loopVideo: true, enableSound: false, tempFilePath: tempFilePath, captureProtected: captureProtected, storeAfterDownload: generateStoreAfterDownload?(message, file))
|
||||
} else {
|
||||
if true || (file.mimeType == "video/mpeg4" || file.mimeType == "video/mov" || file.mimeType == "video/mp4") {
|
||||
if !"".isEmpty && context.sharedContext.immediateExperimentalUISettings.liveStreamV2 {
|
||||
content = PlatformVideoContent(id: .message(message.id, message.stableId, file.fileId), userLocation: .peer(message.id.peerId), content: .file(.message(message: MessageReference(message), media: file)), streamVideo: streamVideos, loopVideo: loopVideos)
|
||||
} else if true || (file.mimeType == "video/mpeg4" || file.mimeType == "video/mov" || file.mimeType == "video/mp4") {
|
||||
content = NativeVideoContent(id: .message(message.stableId, file.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: file), imageReference: mediaImage.flatMap({ ImageMediaReference.message(message: MessageReference(message), media: $0) }), streamVideo: .conservative, loopVideo: loopVideos, tempFilePath: tempFilePath, captureProtected: captureProtected, storeAfterDownload: generateStoreAfterDownload?(message, file))
|
||||
} else {
|
||||
content = PlatformVideoContent(id: .message(message.id, message.stableId, file.fileId), userLocation: .peer(message.id.peerId), content: .file(.message(message: MessageReference(message), media: file)), streamVideo: streamVideos, loopVideo: loopVideos)
|
||||
|
|
|
|||
|
|
@ -111,6 +111,8 @@ swift_library(
|
|||
"//submodules/ImageBlur",
|
||||
"//submodules/MetalEngine",
|
||||
"//submodules/TelegramUI/Components/Calls/VoiceChatActionButton",
|
||||
"//submodules/FFMpegBinding",
|
||||
"//submodules/TelegramCallsUI/CHTTPParser",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
|
|
|||
25
submodules/TelegramCallsUI/CHTTPParser/BUILD
Normal file
25
submodules/TelegramCallsUI/CHTTPParser/BUILD
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
|
||||
objc_library(
|
||||
name = "CHTTPParser",
|
||||
enable_modules = True,
|
||||
module_name = "CHTTPParser",
|
||||
srcs = glob([
|
||||
"Sources/**/*.c",
|
||||
"Sources/**/*.h",
|
||||
]),
|
||||
hdrs = glob([
|
||||
"PublicHeaders/**/*.h",
|
||||
]),
|
||||
includes = [
|
||||
"PublicHeaders",
|
||||
],
|
||||
copts = [
|
||||
"-I{}/PublicHeaders/CHTTPParser".format(package_name()),
|
||||
],
|
||||
sdk_frameworks = [
|
||||
"Foundation",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
|
|
@ -0,0 +1,443 @@
|
|||
/* Copyright Joyent, Inc. and other Node contributors. All rights reserved.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to
|
||||
* deal in the Software without restriction, including without limitation the
|
||||
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
* sell copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
* IN THE SOFTWARE.
|
||||
*/
|
||||
#ifndef http_parser_h
|
||||
#define http_parser_h
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* Also update SONAME in the Makefile whenever you change these. */
|
||||
#define HTTP_PARSER_VERSION_MAJOR 2
|
||||
#define HTTP_PARSER_VERSION_MINOR 9
|
||||
#define HTTP_PARSER_VERSION_PATCH 4
|
||||
|
||||
#include <stddef.h>
|
||||
#if defined(_WIN32) && !defined(__MINGW32__) && \
|
||||
(!defined(_MSC_VER) || _MSC_VER<1600) && !defined(__WINE__)
|
||||
#include <BaseTsd.h>
|
||||
typedef __int8 int8_t;
|
||||
typedef unsigned __int8 uint8_t;
|
||||
typedef __int16 int16_t;
|
||||
typedef unsigned __int16 uint16_t;
|
||||
typedef __int32 int32_t;
|
||||
typedef unsigned __int32 uint32_t;
|
||||
typedef __int64 int64_t;
|
||||
typedef unsigned __int64 uint64_t;
|
||||
#else
|
||||
#include <stdint.h>
|
||||
#endif
|
||||
|
||||
/* Compile with -DHTTP_PARSER_STRICT=0 to make less checks, but run
|
||||
* faster
|
||||
*/
|
||||
#ifndef HTTP_PARSER_STRICT
|
||||
# define HTTP_PARSER_STRICT 1
|
||||
#endif
|
||||
|
||||
/* Maximium header size allowed. If the macro is not defined
|
||||
* before including this header then the default is used. To
|
||||
* change the maximum header size, define the macro in the build
|
||||
* environment (e.g. -DHTTP_MAX_HEADER_SIZE=<value>). To remove
|
||||
* the effective limit on the size of the header, define the macro
|
||||
* to a very large number (e.g. -DHTTP_MAX_HEADER_SIZE=0x7fffffff)
|
||||
*/
|
||||
#ifndef HTTP_MAX_HEADER_SIZE
|
||||
# define HTTP_MAX_HEADER_SIZE (80*1024)
|
||||
#endif
|
||||
|
||||
typedef struct http_parser http_parser;
|
||||
typedef struct http_parser_settings http_parser_settings;
|
||||
|
||||
|
||||
/* Callbacks should return non-zero to indicate an error. The parser will
|
||||
* then halt execution.
|
||||
*
|
||||
* The one exception is on_headers_complete. In a HTTP_RESPONSE parser
|
||||
* returning '1' from on_headers_complete will tell the parser that it
|
||||
* should not expect a body. This is used when receiving a response to a
|
||||
* HEAD request which may contain 'Content-Length' or 'Transfer-Encoding:
|
||||
* chunked' headers that indicate the presence of a body.
|
||||
*
|
||||
* Returning `2` from on_headers_complete will tell parser that it should not
|
||||
* expect neither a body nor any futher responses on this connection. This is
|
||||
* useful for handling responses to a CONNECT request which may not contain
|
||||
* `Upgrade` or `Connection: upgrade` headers.
|
||||
*
|
||||
* http_data_cb does not return data chunks. It will be called arbitrarily
|
||||
* many times for each string. E.G. you might get 10 callbacks for "on_url"
|
||||
* each providing just a few characters more data.
|
||||
*/
|
||||
typedef int (*http_data_cb) (http_parser*, const char *at, size_t length);
|
||||
typedef int (*http_cb) (http_parser*);
|
||||
|
||||
|
||||
/* Status Codes */
|
||||
#define HTTP_STATUS_MAP(XX) \
|
||||
XX(100, CONTINUE, Continue) \
|
||||
XX(101, SWITCHING_PROTOCOLS, Switching Protocols) \
|
||||
XX(102, PROCESSING, Processing) \
|
||||
XX(200, OK, OK) \
|
||||
XX(201, CREATED, Created) \
|
||||
XX(202, ACCEPTED, Accepted) \
|
||||
XX(203, NON_AUTHORITATIVE_INFORMATION, Non-Authoritative Information) \
|
||||
XX(204, NO_CONTENT, No Content) \
|
||||
XX(205, RESET_CONTENT, Reset Content) \
|
||||
XX(206, PARTIAL_CONTENT, Partial Content) \
|
||||
XX(207, MULTI_STATUS, Multi-Status) \
|
||||
XX(208, ALREADY_REPORTED, Already Reported) \
|
||||
XX(226, IM_USED, IM Used) \
|
||||
XX(300, MULTIPLE_CHOICES, Multiple Choices) \
|
||||
XX(301, MOVED_PERMANENTLY, Moved Permanently) \
|
||||
XX(302, FOUND, Found) \
|
||||
XX(303, SEE_OTHER, See Other) \
|
||||
XX(304, NOT_MODIFIED, Not Modified) \
|
||||
XX(305, USE_PROXY, Use Proxy) \
|
||||
XX(307, TEMPORARY_REDIRECT, Temporary Redirect) \
|
||||
XX(308, PERMANENT_REDIRECT, Permanent Redirect) \
|
||||
XX(400, BAD_REQUEST, Bad Request) \
|
||||
XX(401, UNAUTHORIZED, Unauthorized) \
|
||||
XX(402, PAYMENT_REQUIRED, Payment Required) \
|
||||
XX(403, FORBIDDEN, Forbidden) \
|
||||
XX(404, NOT_FOUND, Not Found) \
|
||||
XX(405, METHOD_NOT_ALLOWED, Method Not Allowed) \
|
||||
XX(406, NOT_ACCEPTABLE, Not Acceptable) \
|
||||
XX(407, PROXY_AUTHENTICATION_REQUIRED, Proxy Authentication Required) \
|
||||
XX(408, REQUEST_TIMEOUT, Request Timeout) \
|
||||
XX(409, CONFLICT, Conflict) \
|
||||
XX(410, GONE, Gone) \
|
||||
XX(411, LENGTH_REQUIRED, Length Required) \
|
||||
XX(412, PRECONDITION_FAILED, Precondition Failed) \
|
||||
XX(413, PAYLOAD_TOO_LARGE, Payload Too Large) \
|
||||
XX(414, URI_TOO_LONG, URI Too Long) \
|
||||
XX(415, UNSUPPORTED_MEDIA_TYPE, Unsupported Media Type) \
|
||||
XX(416, RANGE_NOT_SATISFIABLE, Range Not Satisfiable) \
|
||||
XX(417, EXPECTATION_FAILED, Expectation Failed) \
|
||||
XX(421, MISDIRECTED_REQUEST, Misdirected Request) \
|
||||
XX(422, UNPROCESSABLE_ENTITY, Unprocessable Entity) \
|
||||
XX(423, LOCKED, Locked) \
|
||||
XX(424, FAILED_DEPENDENCY, Failed Dependency) \
|
||||
XX(426, UPGRADE_REQUIRED, Upgrade Required) \
|
||||
XX(428, PRECONDITION_REQUIRED, Precondition Required) \
|
||||
XX(429, TOO_MANY_REQUESTS, Too Many Requests) \
|
||||
XX(431, REQUEST_HEADER_FIELDS_TOO_LARGE, Request Header Fields Too Large) \
|
||||
XX(451, UNAVAILABLE_FOR_LEGAL_REASONS, Unavailable For Legal Reasons) \
|
||||
XX(500, INTERNAL_SERVER_ERROR, Internal Server Error) \
|
||||
XX(501, NOT_IMPLEMENTED, Not Implemented) \
|
||||
XX(502, BAD_GATEWAY, Bad Gateway) \
|
||||
XX(503, SERVICE_UNAVAILABLE, Service Unavailable) \
|
||||
XX(504, GATEWAY_TIMEOUT, Gateway Timeout) \
|
||||
XX(505, HTTP_VERSION_NOT_SUPPORTED, HTTP Version Not Supported) \
|
||||
XX(506, VARIANT_ALSO_NEGOTIATES, Variant Also Negotiates) \
|
||||
XX(507, INSUFFICIENT_STORAGE, Insufficient Storage) \
|
||||
XX(508, LOOP_DETECTED, Loop Detected) \
|
||||
XX(510, NOT_EXTENDED, Not Extended) \
|
||||
XX(511, NETWORK_AUTHENTICATION_REQUIRED, Network Authentication Required) \
|
||||
|
||||
enum http_status
|
||||
{
|
||||
#define XX(num, name, string) HTTP_STATUS_##name = num,
|
||||
HTTP_STATUS_MAP(XX)
|
||||
#undef XX
|
||||
};
|
||||
|
||||
|
||||
/* Request Methods */
|
||||
#define HTTP_METHOD_MAP(XX) \
|
||||
XX(0, DELETE, DELETE) \
|
||||
XX(1, GET, GET) \
|
||||
XX(2, HEAD, HEAD) \
|
||||
XX(3, POST, POST) \
|
||||
XX(4, PUT, PUT) \
|
||||
/* pathological */ \
|
||||
XX(5, CONNECT, CONNECT) \
|
||||
XX(6, OPTIONS, OPTIONS) \
|
||||
XX(7, TRACE, TRACE) \
|
||||
/* WebDAV */ \
|
||||
XX(8, COPY, COPY) \
|
||||
XX(9, LOCK, LOCK) \
|
||||
XX(10, MKCOL, MKCOL) \
|
||||
XX(11, MOVE, MOVE) \
|
||||
XX(12, PROPFIND, PROPFIND) \
|
||||
XX(13, PROPPATCH, PROPPATCH) \
|
||||
XX(14, SEARCH, SEARCH) \
|
||||
XX(15, UNLOCK, UNLOCK) \
|
||||
XX(16, BIND, BIND) \
|
||||
XX(17, REBIND, REBIND) \
|
||||
XX(18, UNBIND, UNBIND) \
|
||||
XX(19, ACL, ACL) \
|
||||
/* subversion */ \
|
||||
XX(20, REPORT, REPORT) \
|
||||
XX(21, MKACTIVITY, MKACTIVITY) \
|
||||
XX(22, CHECKOUT, CHECKOUT) \
|
||||
XX(23, MERGE, MERGE) \
|
||||
/* upnp */ \
|
||||
XX(24, MSEARCH, M-SEARCH) \
|
||||
XX(25, NOTIFY, NOTIFY) \
|
||||
XX(26, SUBSCRIBE, SUBSCRIBE) \
|
||||
XX(27, UNSUBSCRIBE, UNSUBSCRIBE) \
|
||||
/* RFC-5789 */ \
|
||||
XX(28, PATCH, PATCH) \
|
||||
XX(29, PURGE, PURGE) \
|
||||
/* CalDAV */ \
|
||||
XX(30, MKCALENDAR, MKCALENDAR) \
|
||||
/* RFC-2068, section 19.6.1.2 */ \
|
||||
XX(31, LINK, LINK) \
|
||||
XX(32, UNLINK, UNLINK) \
|
||||
/* icecast */ \
|
||||
XX(33, SOURCE, SOURCE) \
|
||||
|
||||
enum http_method
|
||||
{
|
||||
#define XX(num, name, string) HTTP_##name = num,
|
||||
HTTP_METHOD_MAP(XX)
|
||||
#undef XX
|
||||
};
|
||||
|
||||
|
||||
enum http_parser_type { HTTP_REQUEST, HTTP_RESPONSE, HTTP_BOTH };
|
||||
|
||||
|
||||
/* Flag values for http_parser.flags field */
|
||||
enum flags
|
||||
{ F_CHUNKED = 1 << 0
|
||||
, F_CONNECTION_KEEP_ALIVE = 1 << 1
|
||||
, F_CONNECTION_CLOSE = 1 << 2
|
||||
, F_CONNECTION_UPGRADE = 1 << 3
|
||||
, F_TRAILING = 1 << 4
|
||||
, F_UPGRADE = 1 << 5
|
||||
, F_SKIPBODY = 1 << 6
|
||||
, F_CONTENTLENGTH = 1 << 7
|
||||
, F_TRANSFER_ENCODING = 1 << 8 /* Never set in http_parser.flags */
|
||||
};
|
||||
|
||||
|
||||
/* Map for errno-related constants
|
||||
*
|
||||
* The provided argument should be a macro that takes 2 arguments.
|
||||
*/
|
||||
#define HTTP_ERRNO_MAP(XX) \
|
||||
/* No error */ \
|
||||
XX(OK, "success") \
|
||||
\
|
||||
/* Callback-related errors */ \
|
||||
XX(CB_message_begin, "the on_message_begin callback failed") \
|
||||
XX(CB_url, "the on_url callback failed") \
|
||||
XX(CB_header_field, "the on_header_field callback failed") \
|
||||
XX(CB_header_value, "the on_header_value callback failed") \
|
||||
XX(CB_headers_complete, "the on_headers_complete callback failed") \
|
||||
XX(CB_body, "the on_body callback failed") \
|
||||
XX(CB_message_complete, "the on_message_complete callback failed") \
|
||||
XX(CB_status, "the on_status callback failed") \
|
||||
XX(CB_chunk_header, "the on_chunk_header callback failed") \
|
||||
XX(CB_chunk_complete, "the on_chunk_complete callback failed") \
|
||||
\
|
||||
/* Parsing-related errors */ \
|
||||
XX(INVALID_EOF_STATE, "stream ended at an unexpected time") \
|
||||
XX(HEADER_OVERFLOW, \
|
||||
"too many header bytes seen; overflow detected") \
|
||||
XX(CLOSED_CONNECTION, \
|
||||
"data received after completed connection: close message") \
|
||||
XX(INVALID_VERSION, "invalid HTTP version") \
|
||||
XX(INVALID_STATUS, "invalid HTTP status code") \
|
||||
XX(INVALID_METHOD, "invalid HTTP method") \
|
||||
XX(INVALID_URL, "invalid URL") \
|
||||
XX(INVALID_HOST, "invalid host") \
|
||||
XX(INVALID_PORT, "invalid port") \
|
||||
XX(INVALID_PATH, "invalid path") \
|
||||
XX(INVALID_QUERY_STRING, "invalid query string") \
|
||||
XX(INVALID_FRAGMENT, "invalid fragment") \
|
||||
XX(LF_EXPECTED, "LF character expected") \
|
||||
XX(INVALID_HEADER_TOKEN, "invalid character in header") \
|
||||
XX(INVALID_CONTENT_LENGTH, \
|
||||
"invalid character in content-length header") \
|
||||
XX(UNEXPECTED_CONTENT_LENGTH, \
|
||||
"unexpected content-length header") \
|
||||
XX(INVALID_CHUNK_SIZE, \
|
||||
"invalid character in chunk size header") \
|
||||
XX(INVALID_CONSTANT, "invalid constant string") \
|
||||
XX(INVALID_INTERNAL_STATE, "encountered unexpected internal state")\
|
||||
XX(STRICT, "strict mode assertion failed") \
|
||||
XX(PAUSED, "parser is paused") \
|
||||
XX(UNKNOWN, "an unknown error occurred") \
|
||||
XX(INVALID_TRANSFER_ENCODING, \
|
||||
"request has invalid transfer-encoding") \
|
||||
|
||||
|
||||
/* Define HPE_* values for each errno value above */
|
||||
#define HTTP_ERRNO_GEN(n, s) HPE_##n,
|
||||
enum http_errno {
|
||||
HTTP_ERRNO_MAP(HTTP_ERRNO_GEN)
|
||||
};
|
||||
#undef HTTP_ERRNO_GEN
|
||||
|
||||
|
||||
/* Get an http_errno value from an http_parser */
|
||||
#define HTTP_PARSER_ERRNO(p) ((enum http_errno) (p)->http_errno)
|
||||
|
||||
|
||||
struct http_parser {
|
||||
/** PRIVATE **/
|
||||
unsigned int type : 2; /* enum http_parser_type */
|
||||
unsigned int flags : 8; /* F_* values from 'flags' enum; semi-public */
|
||||
unsigned int state : 7; /* enum state from http_parser.c */
|
||||
unsigned int header_state : 7; /* enum header_state from http_parser.c */
|
||||
unsigned int index : 5; /* index into current matcher */
|
||||
unsigned int extra_flags : 2;
|
||||
unsigned int lenient_http_headers : 1;
|
||||
|
||||
uint32_t nread; /* # bytes read in various scenarios */
|
||||
uint64_t content_length; /* # bytes in body (0 if no Content-Length header) */
|
||||
|
||||
/** READ-ONLY **/
|
||||
unsigned short http_major;
|
||||
unsigned short http_minor;
|
||||
unsigned int status_code : 16; /* responses only */
|
||||
unsigned int method : 8; /* requests only */
|
||||
unsigned int http_errno : 7;
|
||||
|
||||
/* 1 = Upgrade header was present and the parser has exited because of that.
|
||||
* 0 = No upgrade header present.
|
||||
* Should be checked when http_parser_execute() returns in addition to
|
||||
* error checking.
|
||||
*/
|
||||
unsigned int upgrade : 1;
|
||||
|
||||
/** PUBLIC **/
|
||||
void *data; /* A pointer to get hook to the "connection" or "socket" object */
|
||||
};
|
||||
|
||||
|
||||
struct http_parser_settings {
|
||||
http_cb on_message_begin;
|
||||
http_data_cb on_url;
|
||||
http_data_cb on_status;
|
||||
http_data_cb on_header_field;
|
||||
http_data_cb on_header_value;
|
||||
http_cb on_headers_complete;
|
||||
http_data_cb on_body;
|
||||
http_cb on_message_complete;
|
||||
/* When on_chunk_header is called, the current chunk length is stored
|
||||
* in parser->content_length.
|
||||
*/
|
||||
http_cb on_chunk_header;
|
||||
http_cb on_chunk_complete;
|
||||
};
|
||||
|
||||
|
||||
enum http_parser_url_fields
|
||||
{ UF_SCHEMA = 0
|
||||
, UF_HOST = 1
|
||||
, UF_PORT = 2
|
||||
, UF_PATH = 3
|
||||
, UF_QUERY = 4
|
||||
, UF_FRAGMENT = 5
|
||||
, UF_USERINFO = 6
|
||||
, UF_MAX = 7
|
||||
};
|
||||
|
||||
|
||||
/* Result structure for http_parser_parse_url().
|
||||
*
|
||||
* Callers should index into field_data[] with UF_* values iff field_set
|
||||
* has the relevant (1 << UF_*) bit set. As a courtesy to clients (and
|
||||
* because we probably have padding left over), we convert any port to
|
||||
* a uint16_t.
|
||||
*/
|
||||
struct http_parser_url {
|
||||
uint16_t field_set; /* Bitmask of (1 << UF_*) values */
|
||||
uint16_t port; /* Converted UF_PORT string */
|
||||
|
||||
struct {
|
||||
uint16_t off; /* Offset into buffer in which field starts */
|
||||
uint16_t len; /* Length of run in buffer */
|
||||
} field_data[UF_MAX];
|
||||
};
|
||||
|
||||
|
||||
/* Returns the library version. Bits 16-23 contain the major version number,
|
||||
* bits 8-15 the minor version number and bits 0-7 the patch level.
|
||||
* Usage example:
|
||||
*
|
||||
* unsigned long version = http_parser_version();
|
||||
* unsigned major = (version >> 16) & 255;
|
||||
* unsigned minor = (version >> 8) & 255;
|
||||
* unsigned patch = version & 255;
|
||||
* printf("http_parser v%u.%u.%u\n", major, minor, patch);
|
||||
*/
|
||||
unsigned long http_parser_version(void);
|
||||
|
||||
void http_parser_init(http_parser *parser, enum http_parser_type type);
|
||||
|
||||
|
||||
/* Initialize http_parser_settings members to 0
|
||||
*/
|
||||
void http_parser_settings_init(http_parser_settings *settings);
|
||||
|
||||
|
||||
/* Executes the parser. Returns number of parsed bytes. Sets
|
||||
* `parser->http_errno` on error. */
|
||||
size_t http_parser_execute(http_parser *parser,
|
||||
const http_parser_settings *settings,
|
||||
const char *data,
|
||||
size_t len);
|
||||
|
||||
|
||||
/* If http_should_keep_alive() in the on_headers_complete or
|
||||
* on_message_complete callback returns 0, then this should be
|
||||
* the last message on the connection.
|
||||
* If you are the server, respond with the "Connection: close" header.
|
||||
* If you are the client, close the connection.
|
||||
*/
|
||||
int http_should_keep_alive(const http_parser *parser);
|
||||
|
||||
/* Returns a string version of the HTTP method. */
|
||||
const char *http_method_str(enum http_method m);
|
||||
|
||||
/* Returns a string version of the HTTP status code. */
|
||||
const char *http_status_str(enum http_status s);
|
||||
|
||||
/* Return a string name of the given error */
|
||||
const char *http_errno_name(enum http_errno err);
|
||||
|
||||
/* Return a string description of the given error */
|
||||
const char *http_errno_description(enum http_errno err);
|
||||
|
||||
/* Initialize all http_parser_url members to 0 */
|
||||
void http_parser_url_init(struct http_parser_url *u);
|
||||
|
||||
/* Parse a URL; return nonzero on failure */
|
||||
int http_parser_parse_url(const char *buf, size_t buflen,
|
||||
int is_connect,
|
||||
struct http_parser_url *u);
|
||||
|
||||
/* Pause or un-pause the parser; a nonzero value pauses */
|
||||
void http_parser_pause(http_parser *parser, int paused);
|
||||
|
||||
/* Checks if this is the final chunk of the body. */
|
||||
int http_body_is_final(const http_parser *parser);
|
||||
|
||||
/* Change the maximum header size provided at compile time. */
|
||||
void http_parser_set_max_header_size(uint32_t size);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
2568
submodules/TelegramCallsUI/CHTTPParser/Sources/CHTTPParser.c
Normal file
2568
submodules/TelegramCallsUI/CHTTPParser/Sources/CHTTPParser.c
Normal file
File diff suppressed because it is too large
Load diff
17
submodules/TelegramCallsUI/Resources/dash_player.html
Normal file
17
submodules/TelegramCallsUI/Resources/dash_player.html
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Dash.js Player</title>
|
||||
<script src="https://cdn.dashjs.org/latest/dash.all.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div>
|
||||
<video id="videoPlayer" controls="false" width="640" height="360"></video>
|
||||
</div>
|
||||
<script>
|
||||
var url = "http://127.0.0.1:8013/out_dash.mpd";
|
||||
var player = dashjs.MediaPlayer().create();
|
||||
player.initialize(document.querySelector("#videoPlayer"), url, true);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import Foundation
|
||||
import WebKit
|
||||
import UIKit
|
||||
import ComponentFlow
|
||||
import AccountContext
|
||||
|
|
@ -6,11 +7,12 @@ import AVKit
|
|||
import MultilineTextComponent
|
||||
import Display
|
||||
import ShimmerEffect
|
||||
|
||||
import TelegramCore
|
||||
import SwiftSignalKit
|
||||
import AvatarNode
|
||||
import Postbox
|
||||
import FFMpegBinding
|
||||
import TelegramVoip
|
||||
|
||||
final class MediaStreamVideoComponent: Component {
|
||||
let call: PresentationGroupCallImpl
|
||||
|
|
@ -157,6 +159,8 @@ final class MediaStreamVideoComponent: Component {
|
|||
private var lastPresentation: UIView?
|
||||
private var pipTrackDisplayLink: CADisplayLink?
|
||||
|
||||
private var livePlayerView: ProxyVideoView?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.blurTintView = UIView()
|
||||
self.blurTintView.backgroundColor = UIColor(white: 0.0, alpha: 0.55)
|
||||
|
|
@ -211,7 +215,7 @@ final class MediaStreamVideoComponent: Component {
|
|||
let needsFadeInAnimation = hadVideo
|
||||
|
||||
if loadingBlurView.superview == nil {
|
||||
addSubview(loadingBlurView)
|
||||
//addSubview(loadingBlurView)
|
||||
if needsFadeInAnimation {
|
||||
let anim = CABasicAnimation(keyPath: "opacity")
|
||||
anim.duration = 0.5
|
||||
|
|
@ -542,6 +546,21 @@ final class MediaStreamVideoComponent: Component {
|
|||
videoFrameUpdateTransition.setFrame(layer: self.videoBlurGradientMask, frame: videoBlurView.bounds)
|
||||
videoFrameUpdateTransition.setFrame(layer: self.videoBlurSolidMask, frame: self.videoBlurGradientMask.bounds)
|
||||
}
|
||||
|
||||
if self.livePlayerView == nil {
|
||||
let livePlayerView = ProxyVideoView(context: component.call.accountContext, call: component.call)
|
||||
self.livePlayerView = livePlayerView
|
||||
livePlayerView.layer.masksToBounds = true
|
||||
self.addSubview(livePlayerView)
|
||||
livePlayerView.frame = newVideoFrame
|
||||
livePlayerView.layer.cornerRadius = videoCornerRadius
|
||||
livePlayerView.update(size: newVideoFrame.size)
|
||||
}
|
||||
if let livePlayerView = self.livePlayerView {
|
||||
videoFrameUpdateTransition.setFrame(view: livePlayerView, frame: newVideoFrame, completion: nil)
|
||||
videoFrameUpdateTransition.setCornerRadius(layer: livePlayerView.layer, cornerRadius: videoCornerRadius)
|
||||
livePlayerView.update(size: newVideoFrame.size)
|
||||
}
|
||||
} else {
|
||||
videoSize = CGSize(width: 16 / 9 * 100.0, height: 100.0).aspectFitted(.init(width: availableSize.width - videoInset * 2, height: availableSize.height))
|
||||
}
|
||||
|
|
@ -601,7 +620,7 @@ final class MediaStreamVideoComponent: Component {
|
|||
}
|
||||
}
|
||||
|
||||
if self.noSignalTimeout {
|
||||
if self.noSignalTimeout, !"".isEmpty {
|
||||
var noSignalTransition = transition
|
||||
let noSignalView: ComponentHostView<Empty>
|
||||
if let current = self.noSignalView {
|
||||
|
|
@ -769,3 +788,178 @@ private final class CustomIntensityVisualEffectView: UIVisualEffectView {
|
|||
animator.stopAnimation(true)
|
||||
}
|
||||
}
|
||||
|
||||
private final class ProxyVideoView: UIView {
|
||||
private let call: PresentationGroupCallImpl
|
||||
private let id: Int64
|
||||
private let player: AVPlayer
|
||||
private let playerItem: AVPlayerItem
|
||||
private let playerLayer: AVPlayerLayer
|
||||
|
||||
private var contextDisposable: Disposable?
|
||||
|
||||
private var failureObserverId: AnyObject?
|
||||
private var errorObserverId: AnyObject?
|
||||
|
||||
private var server: AnyObject?
|
||||
|
||||
init(context: AccountContext, call: PresentationGroupCallImpl) {
|
||||
self.call = call
|
||||
|
||||
self.id = Int64.random(in: Int64.min ... Int64.max)
|
||||
|
||||
/*if #available(iOS 13.0, *) {
|
||||
do {
|
||||
let server = try HTTPServer(port: NWEndpoint.Port(integerLiteral: 8012), tcpOptions: nil, queue: .main, handler: { request, response in
|
||||
if request.url == "/master.m3u8" {
|
||||
let _ = (call.externalMediaStream.get()
|
||||
|> take(1)
|
||||
|> mapToSignal { externalMediaStream in
|
||||
return externalMediaStream.masterPlaylistData()
|
||||
}
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { masterPlaylistData in
|
||||
response.send(masterPlaylistData.data(using: .utf8)!)
|
||||
})
|
||||
} else if request.url == "/hls_level_0.m3u8" {
|
||||
let _ = (call.externalMediaStream.get()
|
||||
|> take(1)
|
||||
|> mapToSignal { externalMediaStream in
|
||||
return externalMediaStream.playlistData(quality: 0)
|
||||
}
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { playlistData in
|
||||
response.send(playlistData.data(using: .utf8)!)
|
||||
})
|
||||
} else if request.url == "/hls_level_1.m3u8" {
|
||||
let _ = (call.externalMediaStream.get()
|
||||
|> take(1)
|
||||
|> mapToSignal { externalMediaStream in
|
||||
return externalMediaStream.playlistData(quality: 1)
|
||||
}
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { playlistData in
|
||||
response.send(playlistData.data(using: .utf8)!)
|
||||
})
|
||||
} else if request.url.hasPrefix("/hls_stream0_") && request.url.hasSuffix(".ts") {
|
||||
if let partIndex = Int(request.url[request.url.index(request.url.startIndex, offsetBy: "/hls_stream0_".count)..<request.url.index(request.url.endIndex, offsetBy: -".ts".count)]) {
|
||||
let _ = (call.externalMediaStream.get()
|
||||
|> take(1)
|
||||
|> mapToSignal { externalMediaStream in
|
||||
return externalMediaStream.partData(index: partIndex, quality: 0)
|
||||
}
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { partData in
|
||||
guard let partData else {
|
||||
return
|
||||
}
|
||||
|
||||
let sourceTempFile = TempBox.shared.tempFile(fileName: "part.mp4")
|
||||
let tempFile = TempBox.shared.tempFile(fileName: "part.ts")
|
||||
defer {
|
||||
TempBox.shared.dispose(sourceTempFile)
|
||||
TempBox.shared.dispose(tempFile)
|
||||
}
|
||||
|
||||
let _ = try? partData.write(to: URL(fileURLWithPath: sourceTempFile.path))
|
||||
|
||||
let sourcePath = sourceTempFile.path
|
||||
FFMpegLiveMuxer.remux(sourcePath, to: tempFile.path, offsetSeconds: Double(partIndex))
|
||||
if let data = try? Data(contentsOf: URL(fileURLWithPath: tempFile.path)) {
|
||||
response.send(data)
|
||||
} else {
|
||||
let _ = try? response.send("Error")
|
||||
}
|
||||
})
|
||||
} else {
|
||||
try response.send("Error")
|
||||
}
|
||||
} else if request.url.hasPrefix("/hls_stream1_") && request.url.hasSuffix(".ts") {
|
||||
if let partIndex = Int(request.url[request.url.index(request.url.startIndex, offsetBy: "/hls_stream1_".count)..<request.url.index(request.url.endIndex, offsetBy: -".ts".count)]) {
|
||||
let _ = (call.externalMediaStream.get()
|
||||
|> take(1)
|
||||
|> mapToSignal { externalMediaStream in
|
||||
return externalMediaStream.partData(index: partIndex, quality: 1)
|
||||
}
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { partData in
|
||||
guard let partData else {
|
||||
return
|
||||
}
|
||||
|
||||
let sourceTempFile = TempBox.shared.tempFile(fileName: "part.mp4")
|
||||
let tempFile = TempBox.shared.tempFile(fileName: "part.ts")
|
||||
defer {
|
||||
TempBox.shared.dispose(sourceTempFile)
|
||||
TempBox.shared.dispose(tempFile)
|
||||
}
|
||||
|
||||
let _ = try? partData.write(to: URL(fileURLWithPath: sourceTempFile.path))
|
||||
|
||||
let sourcePath = sourceTempFile.path
|
||||
FFMpegLiveMuxer.remux(sourcePath, to: tempFile.path, offsetSeconds: Double(partIndex))
|
||||
if let data = try? Data(contentsOf: URL(fileURLWithPath: tempFile.path)) {
|
||||
response.send(data)
|
||||
} else {
|
||||
let _ = try? response.send("Error")
|
||||
}
|
||||
})
|
||||
} else {
|
||||
try response.send("Error")
|
||||
}
|
||||
} else {
|
||||
try response.send("Error")
|
||||
}
|
||||
})
|
||||
self.server = server
|
||||
server.resume()
|
||||
} catch let e {
|
||||
print("HTTPServer start error: \(e)")
|
||||
}
|
||||
}*/
|
||||
|
||||
let assetUrl = "http://127.0.0.1:\(SharedHLSServer.shared.port)/\(call.internalId)/master.m3u8"
|
||||
Logger.shared.log("MediaStreamVideoComponent", "Initializing HLS asset at \(assetUrl)")
|
||||
#if DEBUG
|
||||
print("Initializing HLS asset at \(assetUrl)")
|
||||
#endif
|
||||
let asset = AVURLAsset(url: URL(string: assetUrl)!, options: [:])
|
||||
self.playerItem = AVPlayerItem(asset: asset)
|
||||
self.player = AVPlayer(playerItem: self.playerItem)
|
||||
self.playerLayer = AVPlayerLayer(player: self.player)
|
||||
|
||||
super.init(frame: CGRect())
|
||||
|
||||
self.failureObserverId = NotificationCenter.default.addObserver(forName: AVPlayerItem.failedToPlayToEndTimeNotification, object: playerItem, queue: .main, using: { notification in
|
||||
print("Player Error: \(notification.description)")
|
||||
})
|
||||
self.errorObserverId = NotificationCenter.default.addObserver(forName: AVPlayerItem.newErrorLogEntryNotification, object: playerItem, queue: .main, using: { notification in
|
||||
print("Player Error: \(notification.description)")
|
||||
})
|
||||
|
||||
self.layer.addSublayer(self.playerLayer)
|
||||
|
||||
//self.contextDisposable = ResourceAdaptor.shared.addContext(id: self.id, context: context, fileReference: fileReference)
|
||||
|
||||
self.player.play()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.contextDisposable?.dispose()
|
||||
if let failureObserverId = self.failureObserverId {
|
||||
NotificationCenter.default.removeObserver(failureObserverId)
|
||||
}
|
||||
if let errorObserverId = self.errorObserverId {
|
||||
NotificationCenter.default.removeObserver(errorObserverId)
|
||||
}
|
||||
}
|
||||
|
||||
func update(size: CGSize) {
|
||||
self.playerLayer.frame = CGRect(origin: CGPoint(), size: size)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,54 @@
|
|||
//
|
||||
// HTTPContext.swift
|
||||
// NWHTTPProtocol
|
||||
//
|
||||
// Copyright © 2020 ZeeZide GmbH. All rights reserved.
|
||||
//
|
||||
|
||||
import class Network.NWProtocolFramer
|
||||
import class Network.NWConnection
|
||||
|
||||
public extension Optional where Wrapped == NWConnection.ContentContext {
|
||||
|
||||
/**
|
||||
* Extract a HTTPProtocol Message from the context.
|
||||
*
|
||||
* Usage:
|
||||
*
|
||||
* guard let message = context.httpMessage else {
|
||||
* print("Connection closed ...")
|
||||
* connection.cancel()
|
||||
* return
|
||||
* }
|
||||
*
|
||||
* if let method = message.method {
|
||||
* print("REQUEST:", method)
|
||||
* }
|
||||
*/
|
||||
@available(iOS 13.0, *)
|
||||
var httpMessage : NWProtocolFramer.Message? { self?.httpMessage }
|
||||
}
|
||||
|
||||
public extension NWConnection.ContentContext {
|
||||
|
||||
/**
|
||||
* Extract a HTTPProtocol Message from the context.
|
||||
*
|
||||
* Usage:
|
||||
*
|
||||
* guard let message = context?.httpMessage else {
|
||||
* print("Connection closed...")
|
||||
* connection.cancel()
|
||||
* return
|
||||
* }
|
||||
*
|
||||
* if let method = message.method {
|
||||
*
|
||||
* }
|
||||
*/
|
||||
@available(iOS 13.0, *)
|
||||
var httpMessage : NWProtocolFramer.Message? {
|
||||
return protocolMetadata(definition: HTTPProtocol.definition)
|
||||
as? NWProtocolFramer.Message
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
//
|
||||
// HTTPMessage.swift
|
||||
// NWHTTPProtocol
|
||||
//
|
||||
// Copyright © 2020 ZeeZide GmbH. All rights reserved.
|
||||
//
|
||||
|
||||
import class Network.NWProtocolFramer
|
||||
|
||||
/**
|
||||
* Message extensions to access the HTTPProtocol metadata, which are:
|
||||
* - method (e.g. "GET")
|
||||
* - path (e.g. "/vaca")
|
||||
* - status (e.g. 402)
|
||||
* - headers (e.g. [ ( "Content-Type", "text/html" ),
|
||||
* ( "ETag", "42" ) ])
|
||||
* - error
|
||||
*/
|
||||
|
||||
@available(iOS 13.0, *)
|
||||
public extension NWProtocolFramer.Message {
|
||||
|
||||
static var httpMessage : NWProtocolFramer.Message {
|
||||
.init(definition: HTTPProtocol.definition)
|
||||
}
|
||||
|
||||
convenience init(method : HTTPProtocol.Method = "GET",
|
||||
path : String, // really a URI
|
||||
headers : HTTPProtocol.Headers = [],
|
||||
shouldKeepAlive : Bool = false)
|
||||
{
|
||||
self.init(definition: HTTPProtocol.definition)
|
||||
self.method = method
|
||||
self.path = path
|
||||
self.headers = headers
|
||||
self.shouldKeepAlive = shouldKeepAlive
|
||||
}
|
||||
|
||||
convenience init(status : HTTPProtocol.Status,
|
||||
headers : HTTPProtocol.Headers = [])
|
||||
{
|
||||
self.init(definition: HTTPProtocol.definition)
|
||||
self.status = status
|
||||
self.headers = headers
|
||||
}
|
||||
|
||||
var method : HTTPProtocol.Method? {
|
||||
set { self["http.method"] = newValue }
|
||||
get { self["http.method"] as? HTTPProtocol.Method }
|
||||
}
|
||||
|
||||
var path : String? {
|
||||
set { self["http.path"] = newValue }
|
||||
get { self["http.path"] as? String }
|
||||
}
|
||||
|
||||
var status : HTTPProtocol.Status? {
|
||||
set { self["http.status"] = newValue }
|
||||
get { self["http.status"] as? HTTPProtocol.Status }
|
||||
}
|
||||
|
||||
var headers : HTTPProtocol.Headers {
|
||||
set { self["http.headers"] = newValue }
|
||||
get { (self["http.headers"] as? HTTPProtocol.Headers) ?? [] }
|
||||
}
|
||||
|
||||
var isEndOfMessage : Bool {
|
||||
set { self["http.eom"] = newValue }
|
||||
get { (self["http.eom"] as? Bool) ?? false }
|
||||
}
|
||||
|
||||
var shouldKeepAlive : Bool {
|
||||
set { self["http.keepalive"] = newValue }
|
||||
get { (self["http.keepalive"] as? Bool) ?? false }
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 13.0, *)
|
||||
public extension NWProtocolFramer.Message {
|
||||
|
||||
internal convenience init(error: HTTPProtocol.Error) {
|
||||
self.init(definition: HTTPProtocol.definition)
|
||||
self["http.error"] = error
|
||||
}
|
||||
|
||||
/**
|
||||
* Contains a `HTTPProtocol.Error` if the parser failed.
|
||||
*/
|
||||
var error : HTTPProtocol.Error? {
|
||||
self["http.error"] as? HTTPProtocol.Error
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,369 @@
|
|||
//
|
||||
// HTTPProtocol.swift
|
||||
// NWHTTPProtocol
|
||||
//
|
||||
// Copyright © 2020 ZeeZide GmbH. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Network
|
||||
import CHTTPParser
|
||||
|
||||
/**
|
||||
* A Network.framework HTTP protocol parser (aka a NWProtocolFramer).
|
||||
*
|
||||
* Just wrapping http_parser.c/h in the API boilerplate required for
|
||||
* Network.framework protocols.
|
||||
*/
|
||||
@available(iOS 13.0, *)
|
||||
public final class HTTPProtocol: NWProtocolFramerImplementation {
|
||||
// Note: We don't really need a framer here because http_parser does all the
|
||||
// framing already.
|
||||
|
||||
public enum Error: Swift.Error {
|
||||
case parserError(name: String, description: String)
|
||||
case unknownMessageType
|
||||
case missingURI
|
||||
case writeFailed(Swift.Error)
|
||||
}
|
||||
|
||||
public typealias Method = String
|
||||
public typealias Header = ( name: String, value: String )
|
||||
public typealias Headers = [ Header ]
|
||||
public typealias Status = Int
|
||||
|
||||
// Not required by protocol, but used for:
|
||||
// - protocol registration in protocol stack
|
||||
// - message object construction
|
||||
// - message object retrieval from context in receive
|
||||
public static let definition =
|
||||
NWProtocolFramer.Definition(implementation: HTTPProtocol.self)
|
||||
|
||||
public static let label = "HTTP"
|
||||
|
||||
private var parser = UnsafeMutablePointer<http_parser>.allocate(capacity: 1)
|
||||
private var settings = http_parser_settings()
|
||||
|
||||
public init(framer: NWProtocolFramer.Instance) {
|
||||
// TBD: maybe we could get the parser mode as an HTTP protocol option?!
|
||||
http_parser_init(parser, HTTP_BOTH)
|
||||
parser.pointee.data = unsafeBitCast(self, to: UnsafeMutableRawPointer.self)
|
||||
|
||||
settings.on_message_begin = { $0?.framer.onBegin() ; return 0 }
|
||||
settings.on_message_complete = { $0?.framer.messageFinished() ; return 0 }
|
||||
settings.on_headers_complete = { $0?.framer.headerFinished() ; return 0 }
|
||||
|
||||
settings.on_url = { parser, data, len in
|
||||
parser?.framer.processDataForState(.url, data, len) ?? 1
|
||||
}
|
||||
settings.on_header_field = { parser, data, len in
|
||||
parser?.framer.processDataForState(.headerName, data, len) ?? 1
|
||||
}
|
||||
settings.on_header_value = { parser, data, len in
|
||||
parser?.framer.processDataForState(.headerValue, data, len) ?? 1
|
||||
}
|
||||
settings.on_body = { parser, data, len in
|
||||
parser?.framer.processDataForState(.body, data, len) ?? 1
|
||||
}
|
||||
}
|
||||
deinit {
|
||||
parser.deallocate()
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Lifecycle
|
||||
|
||||
public func start (framer: NWProtocolFramer.Instance)
|
||||
-> NWProtocolFramer.StartResult { return .ready }
|
||||
public func stop (framer: NWProtocolFramer.Instance) -> Bool { return true }
|
||||
public func wakeup (framer: NWProtocolFramer.Instance) {}
|
||||
public func cleanup(framer: NWProtocolFramer.Instance) {}
|
||||
|
||||
|
||||
// MARK: - Parsing
|
||||
|
||||
private enum ParseState {
|
||||
case idle, url, headerName, headerValue, body
|
||||
}
|
||||
|
||||
private var parseState = ParseState.idle
|
||||
private var url : String?
|
||||
private var lastName : String?
|
||||
private var headers = [ ( name: String, value: String) ]()
|
||||
private var buffer = Data(capacity: 500)
|
||||
private var activeFramer : NWProtocolFramer.Instance?
|
||||
|
||||
private func onBegin() {
|
||||
clearState()
|
||||
}
|
||||
|
||||
private func clearState() {
|
||||
self.url = nil
|
||||
self.lastName = nil
|
||||
self.headers.removeAll(keepingCapacity: true)
|
||||
}
|
||||
|
||||
private func emit(_ message: NWProtocolFramer.Message,
|
||||
to framer: NWProtocolFramer.Instance)
|
||||
{
|
||||
let ok = framer.deliverInputNoCopy(length : 0,
|
||||
message : message,
|
||||
isComplete : true)
|
||||
assert(ok)
|
||||
}
|
||||
private func emit(_ data : Data,
|
||||
to framer : NWProtocolFramer.Instance)
|
||||
{
|
||||
let message = NWProtocolFramer.Message.httpMessage
|
||||
message.isEndOfMessage = http_body_is_final (parser) != 0
|
||||
message.shouldKeepAlive = http_should_keep_alive(parser) != 0
|
||||
framer.deliverInput(data: data, message: message, isComplete: true)
|
||||
}
|
||||
private func emit(_ data : UnsafeBufferPointer<Int8>,
|
||||
to framer : NWProtocolFramer.Instance)
|
||||
{
|
||||
emit(Data(buffer: data), to: framer)
|
||||
}
|
||||
|
||||
private func headerFinished() {
|
||||
_ = processDataForState(.body, nil, 0) // start body
|
||||
|
||||
assert(activeFramer != nil)
|
||||
guard let framer = activeFramer else { return }
|
||||
|
||||
// We could also do HTTP major/minor too, not really important.
|
||||
|
||||
if isRequest {
|
||||
assert(!isResponse)
|
||||
defer { clearState()}
|
||||
|
||||
|
||||
let method = http_method(CUnsignedInt(parser.pointee.method)).stringValue
|
||||
guard let url = url else {
|
||||
return emit(.init(error: .missingURI), to: framer)
|
||||
}
|
||||
return emit(.init(method: method, path: url, headers: headers,
|
||||
shouldKeepAlive: http_should_keep_alive(parser) != 0),
|
||||
to: framer)
|
||||
}
|
||||
|
||||
if isResponse {
|
||||
assert(!isResponse)
|
||||
defer { clearState()}
|
||||
|
||||
let status = Int(parser.pointee.status_code)
|
||||
return emit(.init(status: status, headers: headers), to: framer)
|
||||
}
|
||||
|
||||
emit(.init(error: .unknownMessageType), to: framer)
|
||||
}
|
||||
|
||||
private func messageFinished() {
|
||||
_ = processDataForState(.idle, nil, 0) // flush end body
|
||||
|
||||
if let framer = activeFramer {
|
||||
// send explicit EOM
|
||||
let message = NWProtocolFramer.Message.httpMessage
|
||||
message.isEndOfMessage = true
|
||||
message.shouldKeepAlive = http_should_keep_alive(parser) != 0
|
||||
emit(message, to: framer)
|
||||
}
|
||||
}
|
||||
|
||||
private var isRequest : Bool { return parser.pointee.type == 0 }
|
||||
private var isResponse : Bool { return parser.pointee.type == 1 }
|
||||
|
||||
private func addData(_ data: UnsafePointer<Int8>?, _ length: Int) -> Int32 {
|
||||
switch parseState {
|
||||
|
||||
case .idle:
|
||||
guard length == 0 else {
|
||||
assertionFailure("receiving data in idle state!")
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
|
||||
case .url, .headerName, .headerValue:
|
||||
guard length > 0 else { return 0 }
|
||||
buffer.append(UnsafeBufferPointer(start: data, count: length))
|
||||
return 0
|
||||
|
||||
case .body:
|
||||
guard let framer = activeFramer else { return 1 }
|
||||
guard length > 0 else { return 1 }
|
||||
assert(activeFramer != nil)
|
||||
|
||||
// TBD: Can we avoid the copying? Maybe. Would need to track more
|
||||
// things (i.e. whether state switches happened).
|
||||
let bptr = UnsafeBufferPointer(start: data, count: length)
|
||||
emit(bptr, to: framer)
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
private func flushOldState(_ oldState: ParseState) -> Int32 {
|
||||
defer { buffer.removeAll() }
|
||||
|
||||
var bufferAsString : String? {
|
||||
return String(data: buffer, encoding: .utf8) // Hm, latin-1? ;-)
|
||||
}
|
||||
|
||||
switch oldState { // finish up OLD parse state
|
||||
case .idle:
|
||||
assert(buffer.isEmpty)
|
||||
guard buffer.isEmpty else { return 1 }
|
||||
|
||||
case .headerValue:
|
||||
assert(lastName != nil)
|
||||
guard let n = lastName, let s = bufferAsString else { return 1 }
|
||||
headers.append( (name: n, value: s) )
|
||||
lastName = nil
|
||||
|
||||
case .headerName:
|
||||
assert(lastName == nil)
|
||||
guard lastName == nil, let s = bufferAsString else { return 1 }
|
||||
lastName = s
|
||||
|
||||
case .url:
|
||||
assert(url == nil)
|
||||
guard url == nil, let s = bufferAsString else { return 1 }
|
||||
url = s
|
||||
|
||||
case .body:
|
||||
if !buffer.isEmpty {
|
||||
guard let framer = activeFramer else { return 1 }
|
||||
emit(buffer, to: framer)
|
||||
}
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
private func processDataForState(_ state: ParseState,
|
||||
_ ptr: UnsafePointer<Int8>?, _ length: Int)
|
||||
-> Int32
|
||||
{
|
||||
if state == parseState { // more data for same field
|
||||
return addData(ptr, length)
|
||||
}
|
||||
|
||||
let rc = flushOldState(parseState)
|
||||
if rc != 0 { return rc }
|
||||
assert(buffer.isEmpty)
|
||||
|
||||
/* start thew new state */
|
||||
parseState = state
|
||||
return addData(ptr, length)
|
||||
}
|
||||
|
||||
public func handleInput(framer: NWProtocolFramer.Instance) -> Int {
|
||||
// Note: http_parser itself does all the framing already
|
||||
assert(activeFramer == nil, "handleInput reentrancy")
|
||||
activeFramer = framer
|
||||
defer { activeFramer = nil }
|
||||
|
||||
_ = framer.parseInput(minimumIncompleteLength: 1,
|
||||
maximumLength: 1024 * 1024)
|
||||
{
|
||||
buffer, isComplete in
|
||||
|
||||
let bytesConsumed = http_parser_execute(
|
||||
parser, &settings,
|
||||
buffer?.baseAddress?.assumingMemoryBound(to: Int8.self),
|
||||
buffer?.count ?? 0
|
||||
)
|
||||
|
||||
let errno = http_errno(parser.pointee.http_errno)
|
||||
if errno != HPE_OK {
|
||||
let error = HTTPProtocol.Error
|
||||
.parserError(name: errno.name, description: errno.description)
|
||||
|
||||
emit(.init(error: error), to: framer)
|
||||
if bytesConsumed < 1 { return buffer?.count ?? 1 } // Hm
|
||||
}
|
||||
|
||||
return bytesConsumed
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Output
|
||||
|
||||
public func handleOutput(framer : NWProtocolFramer.Instance,
|
||||
message : NWProtocolFramer.Message,
|
||||
messageLength : Int,
|
||||
isComplete : Bool)
|
||||
{
|
||||
// We can also support trailers and chunking, but would need to
|
||||
// track more output state.
|
||||
|
||||
assert(isComplete)
|
||||
|
||||
func headerData(with firstLine: String) -> Data {
|
||||
var header = firstLine
|
||||
message.headers.forEach { ( name, value ) in
|
||||
header += name + ": " + value + "\r\n"
|
||||
}
|
||||
header += "\r\n"
|
||||
guard let data = header.data(using: .utf8) else {
|
||||
assertionFailure("could not encode header data?")
|
||||
return Data()
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
if let status = message.status {
|
||||
assert(message.method == nil, "message has status AND method?!")
|
||||
let reason = http_status(UInt32(status)).reason
|
||||
let data = headerData(
|
||||
with: "HTTP/1.1 \(status) \(reason)\r\n")
|
||||
assert(!data.isEmpty)
|
||||
framer.writeOutput(data: data)
|
||||
}
|
||||
else if let method = message.method {
|
||||
assert(message.status == nil, "message has status AND method?!")
|
||||
assert(message.path == nil, "method but no path?")
|
||||
let uri = message.path ?? "/"
|
||||
let data = headerData(with: "\(method) \(uri) HTTP/1.1\r\n")
|
||||
assert(!data.isEmpty)
|
||||
framer.writeOutput(data: data)
|
||||
}
|
||||
|
||||
// body data (the user can also include some in the original message)
|
||||
if messageLength > 0 {
|
||||
do {
|
||||
assert(messageLength > 0)
|
||||
try framer.writeOutputNoCopy(length: messageLength)
|
||||
}
|
||||
catch {
|
||||
assertionFailure("write error: \(error)")
|
||||
let error = HTTPProtocol.Error.writeFailed(error)
|
||||
emit(.init(error: error), to: framer)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Helper Extensions
|
||||
|
||||
fileprivate extension http_method {
|
||||
var stringValue : String { return String(cString: http_method_str(self)) }
|
||||
}
|
||||
fileprivate extension http_status {
|
||||
var reason : String { return String(cString: http_status_str(self)) }
|
||||
}
|
||||
fileprivate extension http_errno {
|
||||
var name : String { return String(cString: http_errno_name(self)) }
|
||||
var description : String {
|
||||
return String(cString: http_errno_description(self))
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 13.0, *)
|
||||
fileprivate extension UnsafeMutablePointer where Pointee == http_parser {
|
||||
var framer : HTTPProtocol {
|
||||
return unsafeBitCast(pointee.data, to: HTTPProtocol.self)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
# NWHTTPProtocol
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
An HTTP protocol parser (aka
|
||||
[NWProtocolFramer](https://developer.apple.com/documentation/network/nwprotocolframer))
|
||||
for the Apple
|
||||
[Network](https://developer.apple.com/documentation/network).framework.
|
||||
|
||||
Network.framework requires iOS 13+ / macOS 10.15+.
|
||||
|
||||
This is intentionally kept very simple and basic. E.g. it does not define
|
||||
Swift types for HTTP requests and the like, but puts the status/request/URI in plain
|
||||
`NWProtocolFramer.Message` metadata fields.
|
||||
|
||||
The protocol is just wrapping the (embedded)
|
||||
[http_parser.c/h](https://github.com/nodejs/http-parser/)
|
||||
developed as part of the Node.js project.
|
||||
|
||||
### Who
|
||||
|
||||
**NWHTTPProtocol** is brought to you by
|
||||
the
|
||||
[Always Right Institute](http://www.alwaysrightinstitute.com)
|
||||
and
|
||||
[ZeeZide](http://zeezide.de).
|
||||
We like
|
||||
[feedback](https://twitter.com/ar_institute),
|
||||
GitHub stars,
|
||||
cool [contract work](http://zeezide.com/en/services/services.html),
|
||||
presumably any form of praise you can think of.
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
//
|
||||
// HTTPMethod.swift
|
||||
// NWHTTPServer
|
||||
//
|
||||
// Copyright © 2020 ZeeZide GmbH. All rights reserved.
|
||||
//
|
||||
|
||||
public struct HTTPMethod: RawRepresentable, Hashable {
|
||||
|
||||
public let rawValue : String
|
||||
|
||||
@inlinable
|
||||
public init(rawValue string: String) { rawValue = string }
|
||||
}
|
||||
|
||||
extension HTTPMethod: CustomStringConvertible {
|
||||
@inlinable
|
||||
public var description: String { return rawValue }
|
||||
}
|
||||
|
||||
@inlinable
|
||||
public func ==(lhs: HTTPMethod, rhs: String) -> Bool {
|
||||
return lhs.rawValue == rhs
|
||||
}
|
||||
@inlinable
|
||||
public func ==(lhs: String, rhs: HTTPMethod) -> Bool {
|
||||
return lhs == rhs.rawValue
|
||||
}
|
||||
|
||||
public extension HTTPMethod {
|
||||
static let GET : HTTPMethod = "GET"
|
||||
static let POST : HTTPMethod = "POST"
|
||||
static let MKCALENDAR : HTTPMethod = "MKCALENDAR"
|
||||
static let DELETE : HTTPMethod = "DELETE"
|
||||
static let HEAD : HTTPMethod = "HEAD"
|
||||
static let PUT : HTTPMethod = "PUT"
|
||||
static let CONNECT : HTTPMethod = "CONNECT"
|
||||
static let OPTIONS : HTTPMethod = "OPTIONS"
|
||||
static let TRACE : HTTPMethod = "TRACE"
|
||||
static let COPY : HTTPMethod = "COPY"
|
||||
static let LOCK : HTTPMethod = "LOCK"
|
||||
static let MKCOL : HTTPMethod = "MKCOL"
|
||||
static let MOVE : HTTPMethod = "MOVE"
|
||||
static let PROPFIND : HTTPMethod = "PROPFIND"
|
||||
static let PROPPATCH : HTTPMethod = "PROPPATCH"
|
||||
static let SEARCH : HTTPMethod = "SEARCH"
|
||||
static let UNLOCK : HTTPMethod = "UNLOCK"
|
||||
static let REPORT : HTTPMethod = "REPORT"
|
||||
static let MKACTIVITY : HTTPMethod = "MKACTIVITY"
|
||||
static let CHECKOUT : HTTPMethod = "CHECKOUT"
|
||||
static let MERGE : HTTPMethod = "MERGE"
|
||||
static let MSEARCH : HTTPMethod = "MSEARCH"
|
||||
static let NOTIFY : HTTPMethod = "NOTIFY"
|
||||
static let SUBSCRIBE : HTTPMethod = "SUBSCRIBE"
|
||||
static let UNSUBSCRIBE : HTTPMethod = "UNSUBSCRIBE"
|
||||
static let PATCH : HTTPMethod = "PATCH"
|
||||
static let PURGE : HTTPMethod = "PURGE"
|
||||
}
|
||||
|
||||
extension HTTPMethod: ExpressibleByStringLiteral {
|
||||
@inlinable
|
||||
public init(stringLiteral string: String) { self.init(rawValue: string) }
|
||||
}
|
||||
|
|
@ -0,0 +1,372 @@
|
|||
//
|
||||
// HTTPServer.swift
|
||||
// NWHTTPServer
|
||||
//
|
||||
// Copyright © 2020-2022 ZeeZide GmbH. All rights reserved.
|
||||
//
|
||||
|
||||
import class Dispatch.DispatchQueue
|
||||
import func Dispatch.dispatchMain
|
||||
import class Network.NWListener
|
||||
import class Network.NWConnection
|
||||
import class Network.NWParameters
|
||||
import class Network.NWProtocolFramer
|
||||
import class Network.NWProtocolTCP
|
||||
import enum Network.NWEndpoint
|
||||
|
||||
/**
|
||||
* A very simple Network.framework based HTTP server.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* let server = HTTPServer { request, response in
|
||||
* print("Received:", request)
|
||||
* try response.send("Hello!\n")
|
||||
* }
|
||||
* server.run()
|
||||
*
|
||||
*/
|
||||
@available(iOS 13.0, *)
|
||||
public final class HTTPServer {
|
||||
|
||||
let queue : DispatchQueue
|
||||
let handler : ( IncomingMessage, ServerResponse ) throws -> Void
|
||||
let listener : NWListener
|
||||
|
||||
private var clients = [ ObjectIdentifier : ConnectionState ]()
|
||||
private var _listenCB : (( HTTPServer ) -> Void)?
|
||||
private var _errorCB : (( HTTPServer, Swift.Error ) -> Void)?
|
||||
|
||||
/**
|
||||
* Initialize an HTTP server.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* let server = HTTPServer { request, response in
|
||||
* print("Received:", request)
|
||||
* try response.send("Hello!\n")
|
||||
* }
|
||||
*
|
||||
* - Parameters:
|
||||
* - port: The TCP port the server should listen on (default: 8000)
|
||||
* - tcpOptions: Optional, alternative Network framework TCP options
|
||||
* - queue: The DispatchQueue the server operates on. This should be
|
||||
* a _serial_ queue. Defaults to `.main`
|
||||
* - handler: Closure to invoke when an HTTP request was received.
|
||||
*/
|
||||
public init(port : NWEndpoint.Port = 8000,
|
||||
tcpOptions : NWProtocolTCP.Options? = nil,
|
||||
queue : DispatchQueue = .main,
|
||||
handler : @escaping
|
||||
( IncomingMessage, ServerResponse ) throws -> Void)
|
||||
throws
|
||||
{
|
||||
self.queue = queue
|
||||
self.handler = handler
|
||||
|
||||
let params : NWParameters = {
|
||||
let params = NWParameters(tls: nil, tcp: tcpOptions ?? {
|
||||
let tcpOptions = NWProtocolTCP.Options()
|
||||
tcpOptions.enableKeepalive = true
|
||||
tcpOptions.keepaliveIdle = 2
|
||||
return tcpOptions
|
||||
}())
|
||||
|
||||
let httpProtocol =
|
||||
NWProtocolFramer.Options(definition: HTTPProtocol.definition)
|
||||
|
||||
params.defaultProtocolStack
|
||||
.applicationProtocols
|
||||
.insert(httpProtocol, at: 0)
|
||||
|
||||
return params
|
||||
}()
|
||||
|
||||
listener = try NWListener(using: params, on: port)
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the server, never returns
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* server.run()
|
||||
*
|
||||
* For more fine grained flow control, use `resume` and `suspend`.
|
||||
*/
|
||||
public func run() -> Never {
|
||||
resume()
|
||||
dispatchMain()
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the server in the background.
|
||||
*
|
||||
* Note: This does not keep a commandline tool (or thread) running, use `run`
|
||||
* to start a server "forever".
|
||||
*/
|
||||
public func resume() {
|
||||
listener.newConnectionHandler = handleNewConnection
|
||||
listener.stateUpdateHandler = handleListenerStateChange
|
||||
listener.start(queue: queue)
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the server and cancels all running connections.
|
||||
*/
|
||||
public func suspend() {
|
||||
listener.cancel()
|
||||
listener.newConnectionHandler = nil
|
||||
listener.stateUpdateHandler = nil
|
||||
|
||||
// TBD: we could also offer a more graceful option
|
||||
for state in clients.values {
|
||||
state.cancel()
|
||||
unregister(state.connection)
|
||||
}
|
||||
assert(clients.isEmpty)
|
||||
clients = [:]
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Events
|
||||
|
||||
/**
|
||||
* Register a callback to be called when the server starts accepting requests.
|
||||
*
|
||||
* - Parameter execute: A closure to run when the server actually started
|
||||
* listening (i.e. can be reached by clients).
|
||||
*/
|
||||
public func onListen(execute: @escaping ( HTTPServer ) -> Void) {
|
||||
_listenCB = execute
|
||||
}
|
||||
|
||||
/**
|
||||
* Register an error handler which will be invoked when errors happen on
|
||||
* either the listener or the connection.
|
||||
*
|
||||
* - Parameter execute: A closure to run when the server encounters an error.
|
||||
*/
|
||||
public func onError(execute: @escaping ( HTTPServer, Swift.Error ) -> Void) {
|
||||
_errorCB = execute
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Connection Handling
|
||||
|
||||
private enum ConnectionState {
|
||||
case idle (NWConnection)
|
||||
case running(NWConnection, IncomingMessage, ServerResponse)
|
||||
|
||||
var connection : NWConnection {
|
||||
switch self {
|
||||
case .idle (let c) : return c
|
||||
case .running(let c, _, _) : return c
|
||||
}
|
||||
}
|
||||
func cancel() {
|
||||
switch self {
|
||||
case .idle(let c):
|
||||
c.cancel()
|
||||
case .running(let c, let m, let res):
|
||||
if !m.readableEnded { m.push(nil) }
|
||||
if !res.writableEnded { res.end() }
|
||||
c.cancel()
|
||||
}
|
||||
}
|
||||
var incomingMessage : IncomingMessage? {
|
||||
switch self {
|
||||
case .idle : return nil
|
||||
case .running(_, let m, _) : return m
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func handleListenerStateChange(_ state: NWListener.State) {
|
||||
switch state {
|
||||
case .setup, .waiting : break
|
||||
case .ready : _listenCB?(self)
|
||||
case .cancelled : suspend()
|
||||
|
||||
case .failed(let error):
|
||||
suspend()
|
||||
_errorCB?(self, error)
|
||||
|
||||
@unknown default: break
|
||||
}
|
||||
}
|
||||
|
||||
private func handleStateChange(_ state: NWConnection.State,
|
||||
on connection: NWConnection)
|
||||
{
|
||||
switch state {
|
||||
case .setup, .preparing, .waiting: break
|
||||
case .ready : readNextMessage(from: connection)
|
||||
case .cancelled : unregister(connection)
|
||||
|
||||
case .failed(let error):
|
||||
unregister(connection)
|
||||
_errorCB?(self, error)
|
||||
|
||||
@unknown default: break
|
||||
}
|
||||
}
|
||||
|
||||
private func unregister(_ connection: NWConnection) {
|
||||
if let state = clients.removeValue(forKey: ObjectIdentifier(connection)) {
|
||||
if case .running(_, let req, let res) = state {
|
||||
if !req.readableEnded { req.push(nil) }
|
||||
if !res.writableEnded { res.end() }
|
||||
}
|
||||
}
|
||||
connection.stateUpdateHandler = nil
|
||||
}
|
||||
|
||||
private func handleNewConnection(_ connection: NWConnection) {
|
||||
let oid = ObjectIdentifier(connection)
|
||||
assert(clients[oid] == nil)
|
||||
clients[oid] = .idle(connection)
|
||||
|
||||
connection.stateUpdateHandler = {
|
||||
self.handleStateChange($0, on: connection)
|
||||
}
|
||||
connection.start(queue: queue)
|
||||
}
|
||||
|
||||
private func readNextMessage(from connection: NWConnection) {
|
||||
let oid = ObjectIdentifier(connection)
|
||||
|
||||
connection.receiveMessage { data, context, isComplete, error in
|
||||
assert(isComplete || error != nil) // right?
|
||||
|
||||
guard let state = self.clients[oid] else { // already cancelled.
|
||||
// can happen when the server is suspended (that cancels all the
|
||||
// connections and drops all states synchronously).
|
||||
return
|
||||
}
|
||||
|
||||
func emitError(_ error: Swift.Error) {
|
||||
if case .running(_, let req, _) = state, req.emitError(error) {
|
||||
// error handled as part of the request
|
||||
}
|
||||
else if let cb = self._errorCB {
|
||||
cb(self, error)
|
||||
}
|
||||
}
|
||||
func endBothEnds() {
|
||||
if case .running(_, let req, let res) = state {
|
||||
if !req.readableEnded { req.push(nil) }
|
||||
if !res.writableEnded { res.end() }
|
||||
}
|
||||
}
|
||||
|
||||
if let error = error {
|
||||
emitError(error)
|
||||
connection.cancel() // no recovery possible?
|
||||
return endBothEnds()
|
||||
}
|
||||
|
||||
guard let message = context.httpMessage else {
|
||||
switch state {
|
||||
case .idle: // hit if the client closes a keep-alive connection
|
||||
connection.cancel()
|
||||
return endBothEnds()
|
||||
case .running(_, let req, _):
|
||||
if !req.readableEnded { req.push(nil) }
|
||||
// Do not close `response`(/connection), we could still be writing!
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if let error = message.error {
|
||||
emitError(error)
|
||||
connection.cancel() // no recovery possible?
|
||||
return endBothEnds()
|
||||
}
|
||||
|
||||
/* HTTP Request HEAD */
|
||||
if let method = message.method, let path = message.path {
|
||||
if case .running(_, let req, _) = state {
|
||||
if !req.readableEnded { req.push(nil) }
|
||||
}
|
||||
|
||||
let req = IncomingMessage(method: .init(rawValue: method), path: path,
|
||||
headers: message.headers)
|
||||
let res = ServerResponse(keepAlive: false)
|
||||
res.connection = connection
|
||||
|
||||
res.onEnd { res in
|
||||
if let state = self.clients[oid],
|
||||
case .running(let connection, _, _) = state,
|
||||
connection === res.connection
|
||||
{
|
||||
self.clients[oid] = .idle(connection)
|
||||
}
|
||||
}
|
||||
|
||||
self.clients[oid] = ConnectionState.running(connection, req, res)
|
||||
|
||||
do {
|
||||
try self.handler(req, res)
|
||||
return self.readNextMessage(from: connection)
|
||||
}
|
||||
catch {
|
||||
emitError(error)
|
||||
if !res.writableEnded && !res.didWriteHead {
|
||||
res.keepAlive = false
|
||||
res.writeHead(status: 500, headers: [ "Connection": "close" ])
|
||||
res.end()
|
||||
}
|
||||
return connection.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
/* HTTP response HEAD */
|
||||
if let _ = message.status {
|
||||
// attempt to send a response to the server :-)
|
||||
switch state {
|
||||
case .idle : break
|
||||
case .running : endBothEnds()
|
||||
}
|
||||
return connection.cancel()
|
||||
}
|
||||
|
||||
|
||||
func endRequest() {
|
||||
if case .running(_, let req, _) = state {
|
||||
if !req.readableEnded { req.push(nil) }
|
||||
}
|
||||
}
|
||||
|
||||
if let data = data {
|
||||
assert(message.status == nil && message.method == nil)
|
||||
switch state {
|
||||
case .idle:
|
||||
assertionFailure("receiving body data, w/o a HEAD?")
|
||||
return connection.cancel()
|
||||
|
||||
case .running(_, let req, let res):
|
||||
res.keepAlive = message.shouldKeepAlive
|
||||
if data.isEmpty {
|
||||
assert(message.isEndOfMessage)
|
||||
endRequest()
|
||||
}
|
||||
else {
|
||||
req.push(data)
|
||||
if message.isEndOfMessage { endRequest() }
|
||||
}
|
||||
return self.readNextMessage(from: connection)
|
||||
}
|
||||
}
|
||||
|
||||
if message.isEndOfMessage {
|
||||
endRequest()
|
||||
return self.readNextMessage(from: connection)
|
||||
}
|
||||
|
||||
assertionFailure("got neither data nor metadata? \(message)")
|
||||
return connection.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
//
|
||||
// HTTPStatus.swift
|
||||
// NWHTTPServer
|
||||
//
|
||||
// Copyright © 2020 ZeeZide GmbH. All rights reserved.
|
||||
//
|
||||
|
||||
|
||||
public struct HTTPStatus: RawRepresentable, Hashable {
|
||||
public let rawValue : Int
|
||||
public init(rawValue status: Int) { self.rawValue = status }
|
||||
}
|
||||
|
||||
extension HTTPStatus {
|
||||
public init(_ status: Int) { self.init(rawValue: status) }
|
||||
}
|
||||
|
||||
extension HTTPStatus: ExpressibleByIntegerLiteral {
|
||||
public init(integerLiteral status: Int) { self.init(rawValue: status) }
|
||||
}
|
||||
|
||||
public extension HTTPStatus {
|
||||
|
||||
static let ok : HTTPStatus = 200
|
||||
static let created : HTTPStatus = 201
|
||||
static let noContent : HTTPStatus = 204
|
||||
|
||||
static let badRequest : HTTPStatus = 400
|
||||
static let paymentRequired : HTTPStatus = 402
|
||||
static let forbidden : HTTPStatus = 403
|
||||
static let notFound : HTTPStatus = 404
|
||||
|
||||
static let serverError : HTTPStatus = 500
|
||||
}
|
||||
|
|
@ -0,0 +1,309 @@
|
|||
//
|
||||
// IncomingMessage.swift
|
||||
// NWHTTPServer
|
||||
//
|
||||
// Copyright © 2020 ZeeZide GmbH. All rights reserved.
|
||||
//
|
||||
|
||||
import struct Foundation.Data
|
||||
import class Foundation.JSONDecoder
|
||||
import class Foundation.JSONSerialization
|
||||
|
||||
/**
|
||||
* Represents an incoming HTTP message.
|
||||
*
|
||||
* This can be both, a Request or a Response - it is a Response when it got
|
||||
* created by a client and it is a Request if it is coming from the Server.
|
||||
*
|
||||
* The content of the message can be either streamed to the client,
|
||||
* or if no data callback is set, it will get buffered within the
|
||||
* object.
|
||||
* In both cases one needs to wait for `end`!
|
||||
*
|
||||
* Content Buffering:
|
||||
*
|
||||
* let server = try HTTPServer { request, response in
|
||||
* print("Request:", request)
|
||||
* request.onEnd {
|
||||
* print("received content:", request.content)
|
||||
* response.send("OK, got it!\n")
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* Content Streaming:
|
||||
*
|
||||
* let server = try HTTPServer { request, response in
|
||||
* print("Request:", request)
|
||||
* request.onData { data in
|
||||
* print("received data chunk:", data)
|
||||
* }
|
||||
* request.onEnd {
|
||||
* response.send("OK, got it!\n")
|
||||
* }
|
||||
* }
|
||||
*
|
||||
*/
|
||||
@available(iOS 13.0, *)
|
||||
open class IncomingMessage: CustomStringConvertible {
|
||||
// Most is marked `open` in case the consumer wants to patch stuff in a
|
||||
// subclass.
|
||||
|
||||
public enum IncomingType {
|
||||
case request (method: HTTPMethod, path: String)
|
||||
case response(status: HTTPStatus)
|
||||
}
|
||||
|
||||
open var messageType : IncomingType
|
||||
open var headers : HTTPProtocol.Headers
|
||||
open var bufferedData = Data()
|
||||
open var readableEnded = false
|
||||
|
||||
open var _errorCB : (( Swift.Error ) -> Void)?
|
||||
open var _dataCB : (( Data ) -> Void)?
|
||||
open var _endCB : (() -> Void)?
|
||||
|
||||
public init(method: HTTPMethod, path: String,
|
||||
headers: HTTPProtocol.Headers = [])
|
||||
{
|
||||
self.messageType = .request(method: method, path: path)
|
||||
self.headers = headers
|
||||
}
|
||||
public init(status: HTTPStatus, headers: HTTPProtocol.Headers = []) {
|
||||
self.messageType = .response(status: status)
|
||||
self.headers = headers
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Callbacks
|
||||
|
||||
/**
|
||||
* Register a callback to be executed when content is received. When a
|
||||
* callback is set, the buffer won't get filled.
|
||||
*
|
||||
* If data was accumulated already, it will be flushed to that closure.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* request.onData { data in
|
||||
* print("received data chunk:", data)
|
||||
* }
|
||||
*
|
||||
*/
|
||||
open func onData(execute: @escaping ( Data ) -> Void) {
|
||||
_dataCB = execute
|
||||
flush() // TBD: async?
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a callback to be executed when the request has been retrieved
|
||||
* completely, i.e. all body data has been read.
|
||||
*/
|
||||
open func onEnd(execute: @escaping () -> Void) {
|
||||
guard !readableEnded else { return execute() } // TBD: async?
|
||||
_endCB = execute
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a callback to be executed when request specific errors arrive.
|
||||
* If none is set, errors will be sent to the HTTPserver error handler.
|
||||
*/
|
||||
open func onError(execute: @escaping ( Swift.Error ) -> Void) {
|
||||
_errorCB = execute
|
||||
}
|
||||
|
||||
internal func emitError(_ error: Swift.Error) -> Bool {
|
||||
guard let cb = _errorCB else { return false }
|
||||
cb(error)
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Receiving Body Data
|
||||
|
||||
private func flush() {
|
||||
guard let dataCB = _dataCB else { return }
|
||||
guard !bufferedData.isEmpty else { return }
|
||||
let data = bufferedData; bufferedData = Data()
|
||||
dataCB(data)
|
||||
}
|
||||
private func invalidate() {
|
||||
_dataCB = nil
|
||||
_endCB = nil
|
||||
_errorCB = nil
|
||||
}
|
||||
|
||||
/**
|
||||
* Push new body data into the request. Push `nil` for end-of-message.
|
||||
*/
|
||||
open func push(_ data: Data?) {
|
||||
assert(!readableEnded)
|
||||
guard !readableEnded else { return }
|
||||
|
||||
guard let data = data else { // EOF
|
||||
readableEnded = true
|
||||
flush()
|
||||
_endCB?()
|
||||
return invalidate()
|
||||
}
|
||||
|
||||
if let dataCB = _dataCB {
|
||||
flush()
|
||||
dataCB(data)
|
||||
}
|
||||
else {
|
||||
bufferedData.append(data)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Content Accessors (when used w/o a callback)
|
||||
|
||||
/**
|
||||
* Returns all body data buffered so far. Note that this will be empty at the
|
||||
* time the HTTPServer handler is invoked!
|
||||
* To wait for all content to arrive in the buffer, use the `onEnd` callback.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* request.onEnd {
|
||||
* print("received content:", request.content)
|
||||
* }
|
||||
*
|
||||
* No data will be buffered if the user has installed an `onData` handler.
|
||||
*/
|
||||
open var content : Data {
|
||||
return bufferedData
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all body data buffered so far. Note that this will be empty at the
|
||||
* time the HTTPServer handler is invoked!
|
||||
* To wait for all content to arrive in the buffer, use the `onEnd` callback.
|
||||
*
|
||||
* This variant tries to return the content as an UTF-8 string. If a
|
||||
* conversion to UTF-8 fails, an error will be emitted and nil will be
|
||||
* returned.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* request.onEnd {
|
||||
* print("received content:", request.contentAsString ?? "-")
|
||||
* }
|
||||
*
|
||||
* No data will be buffered if the user has installed an `onData` handler.
|
||||
*/
|
||||
open var contentAsString : String? {
|
||||
// TODO: scan for charset in headers :-)
|
||||
guard !content.isEmpty else { return "" }
|
||||
guard let s = String(data: content, encoding: .utf8) else {
|
||||
_errorCB?(StringEncodingError(encoding: .utf8))
|
||||
return nil
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all body data buffered so far. Note that this will be empty at the
|
||||
* time the HTTPServer handler is invoked!
|
||||
* To wait for all content to arrive in the buffer, use the `onEnd` callback.
|
||||
*
|
||||
* This variant tries to parse the content as JSON into a Decodable type
|
||||
* provided.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* struct Entry: Codable {
|
||||
* let date : Date
|
||||
* let title : String
|
||||
* let body : String
|
||||
* }
|
||||
*
|
||||
* request.onEnd {
|
||||
* guard let entry = try? request.decodeJSON(as: Entry.self) else {
|
||||
* response.writeHead(status: badRequest)
|
||||
* response.end()
|
||||
* return
|
||||
* }
|
||||
* print("received entry:", entry)
|
||||
* response.send("got entry!")
|
||||
* }
|
||||
*
|
||||
* No data will be buffered if the user has installed an `onData` handler.
|
||||
*/
|
||||
open func decodeJSON<T: Decodable>(as type: T.Type) throws -> T {
|
||||
return try JSONDecoder().decode(type, from: content)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all body data buffered so far. Note that this will be empty at the
|
||||
* time the HTTPServer handler is invoked!
|
||||
* To wait for all content to arrive in the buffer, use the `onEnd` callback.
|
||||
*
|
||||
* This variant tries to parse the content as JSON into property list values.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* request.onEnd {
|
||||
* guard let entry = try? request.decodeJSON()
|
||||
* as? [ String : String] else
|
||||
* {
|
||||
* response.writeHead(status: badRequest)
|
||||
* response.end()
|
||||
* return
|
||||
* }
|
||||
* print("received entry:", entry)
|
||||
* response.send("got entry!")
|
||||
* }
|
||||
*
|
||||
* No data will be buffered if the user has installed an `onData` handler.
|
||||
*/
|
||||
open func decodeJSON(options : JSONSerialization.ReadingOptions = []) throws
|
||||
-> Any
|
||||
{
|
||||
return try JSONSerialization.jsonObject(with: content, options: options)
|
||||
}
|
||||
|
||||
|
||||
// MARK: - HTTP Requests
|
||||
|
||||
@inlinable
|
||||
public var method : HTTPMethod {
|
||||
guard case .request(let method, _) = messageType else { return "" }
|
||||
return method
|
||||
}
|
||||
|
||||
@inlinable
|
||||
public var url : String {
|
||||
guard case .request(_, let path) = messageType else { return "" }
|
||||
return path
|
||||
}
|
||||
|
||||
// MARK: - HTTP Responses
|
||||
|
||||
@inlinable
|
||||
public var statusCode : Int {
|
||||
guard case .response(let status) = messageType else { return 0 }
|
||||
return status.rawValue
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Description
|
||||
|
||||
open var description: String {
|
||||
var ms = "<"
|
||||
|
||||
switch messageType {
|
||||
case .request(let method, let path):
|
||||
ms += method.rawValue + ": " + path
|
||||
case .response(let status):
|
||||
ms += "\(status)"
|
||||
}
|
||||
|
||||
headers.forEach { ( name, value ) in
|
||||
ms += " " + name + "=" + value
|
||||
}
|
||||
|
||||
ms += ">"
|
||||
return ms
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
# NWHTTPServer
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
A very simple HTTP server
|
||||
for the Apple
|
||||
[Network](https://developer.apple.com/documentation/network).framework.
|
||||
Based on the `NWHTTPProtocol`.
|
||||
|
||||
Example:
|
||||
```swift
|
||||
let server = HTTPServer { request, response in
|
||||
print("Received:", request)
|
||||
try response.send("Hello!\n")
|
||||
}
|
||||
server.run()
|
||||
```
|
||||
|
||||
### Who
|
||||
|
||||
**NWHTTPProtocol** is brought to you by
|
||||
the
|
||||
[Always Right Institute](http://www.alwaysrightinstitute.com)
|
||||
and
|
||||
[ZeeZide](http://zeezide.de).
|
||||
We like
|
||||
[feedback](https://twitter.com/ar_institute),
|
||||
GitHub stars,
|
||||
cool [contract work](http://zeezide.com/en/services/services.html),
|
||||
presumably any form of praise you can think of.
|
||||
|
|
@ -0,0 +1,396 @@
|
|||
//
|
||||
// ServerResponse.swift
|
||||
// NWHTTPServer
|
||||
//
|
||||
// Copyright © 2020 ZeeZide GmbH. All rights reserved.
|
||||
//
|
||||
|
||||
import struct Foundation.Data
|
||||
import class Foundation.JSONEncoder
|
||||
import class Foundation.JSONSerialization
|
||||
import class Network.NWConnection
|
||||
import class Network.NWProtocolFramer
|
||||
import enum Network.NWError
|
||||
|
||||
/**
|
||||
* Represents an HTTP response sent by the server.
|
||||
*
|
||||
* `ServerResponse` is not usually created by user code, it is provided to
|
||||
* the HTTPServer handler closure when a request is initiated.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* let server = HTTPServer { request, response in
|
||||
* try response.send("Hello!\n")
|
||||
* }
|
||||
* server.run()
|
||||
*
|
||||
* The `send` functions are convenience which setup things like Content-Length
|
||||
* and Content-Type headers.
|
||||
*
|
||||
* When using the lower level `write` function, make sure to properly `end`
|
||||
* the request!
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* let server = HTTPServer { request, response in
|
||||
* response.write("Hello" .data(using: .utf8)!)
|
||||
* response.write(" World".data(using: .utf8)!)
|
||||
* response.keepAlive = false // because we set no content-length
|
||||
* response.end() // important!
|
||||
* }
|
||||
* server.run()
|
||||
*
|
||||
*/
|
||||
|
||||
@available(iOS 13.0, *)
|
||||
open class ServerResponse {
|
||||
// Most is marked `open` in case the consumer wants to patch stuff in a
|
||||
// subclass.
|
||||
|
||||
open var connection : NWConnection?
|
||||
open var keepAlive : Bool
|
||||
open var status : HTTPStatus
|
||||
open var headers : [ String : String ]
|
||||
|
||||
open var didWriteHead = false
|
||||
open var content : Data?
|
||||
open var writableEnded = false
|
||||
|
||||
open var _endHandlers = [ ( ServerResponse ) -> Void ]()
|
||||
|
||||
/**
|
||||
* Setup a new `ServerResponse`.
|
||||
*
|
||||
* `ServerResponse` is not usually created by user code, it is provided to
|
||||
* the HTTPServer handler closure when a request is initiated.
|
||||
*/
|
||||
public init(status : HTTPStatus = 200,
|
||||
headers : [ String : String ] = [:],
|
||||
keepAlive : Bool = false)
|
||||
{
|
||||
self.status = status
|
||||
self.headers = headers
|
||||
self.keepAlive = keepAlive
|
||||
if !keepAlive && self.headers["Connection"] == nil {
|
||||
self.headers["Connection"] = "close"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Event Handlers
|
||||
|
||||
// Note: We could add onError, but we can't really do much in this case?
|
||||
// It currently just cancels the connection which should teardown
|
||||
// everything.
|
||||
|
||||
/**
|
||||
* Register closures to be executed when the response has been fully written
|
||||
* to the client. I.e. if `end` has been called and successfully executed.
|
||||
*/
|
||||
open func onEnd(execute: @escaping ( ServerResponse ) -> Void) {
|
||||
guard !writableEnded else {
|
||||
return execute(self) // TBD: async this?
|
||||
}
|
||||
_endHandlers.append(execute)
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Writing Response Data
|
||||
|
||||
/**
|
||||
* Write the HTTP response head to the connection.
|
||||
*
|
||||
* Status and headers can be provided as arguments, and are combined w/
|
||||
* the values set in the response object.
|
||||
*
|
||||
* Consider using the higher level `send` functions instead.
|
||||
*/
|
||||
open func writeHead(status : HTTPStatus? = nil,
|
||||
headers : [ String : String ] = [:])
|
||||
{
|
||||
assert(!didWriteHead)
|
||||
assert(!writableEnded)
|
||||
|
||||
guard let connection = connection else {
|
||||
if let status = status { self.status = status }
|
||||
for ( name, value ) in headers {
|
||||
self.headers[name] = value
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
didWriteHead = true
|
||||
|
||||
var headerArray = HTTPProtocol.Headers()
|
||||
headerArray.reserveCapacity(headers.count + self.headers.count)
|
||||
for ( name, value ) in self.headers {
|
||||
guard headers[name] == nil else { continue }
|
||||
headerArray.append( ( name: name, value: value ) )
|
||||
}
|
||||
for ( name, value ) in headers {
|
||||
headerArray.append( ( name: name, value: value ) )
|
||||
}
|
||||
|
||||
let message = NWProtocolFramer.Message(status : status?.rawValue
|
||||
?? self.status.rawValue,
|
||||
headers : headerArray)
|
||||
let context = NWConnection
|
||||
.ContentContext(identifier: "HTTPResponseHead", metadata: [ message ])
|
||||
|
||||
connection.send(content: nil,
|
||||
contentContext: context, isComplete: true,
|
||||
completion: .contentProcessed({ error in
|
||||
if let error = error { self.handleWriteError(error) }
|
||||
}))
|
||||
|
||||
flushIfPossible()
|
||||
}
|
||||
|
||||
/**
|
||||
* Write HTTP body data to the connection.
|
||||
*
|
||||
* If the response head has not been written yet, it will be now.
|
||||
*
|
||||
* Important: Once finished writing to the response, call `end` to mark
|
||||
* the response as complete.
|
||||
*
|
||||
* Consider using the higher level `send` functions instead.
|
||||
*/
|
||||
@discardableResult
|
||||
open func write(_ data: Data) -> Bool {
|
||||
assert(!writableEnded)
|
||||
guard let connection = connection else {
|
||||
if content?.append(data) == nil { content = data }
|
||||
let hwm = 1000
|
||||
return (content?.count ?? 0) < hwm
|
||||
}
|
||||
|
||||
if !didWriteHead { writeHead() }
|
||||
flushIfPossible()
|
||||
guard !data.isEmpty else { return true }
|
||||
|
||||
let message = NWProtocolFramer.Message.httpMessage
|
||||
let context = NWConnection
|
||||
.ContentContext(identifier: "HTTPResponseBody", metadata: [ message ])
|
||||
|
||||
connection.send(content: data,
|
||||
contentContext: context, isComplete: true,
|
||||
completion: .contentProcessed({ error in
|
||||
if let error = error { self.handleWriteError(error) }
|
||||
}))
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Finish the response.
|
||||
*
|
||||
* If the response head has not been written yet, it will be now, the same
|
||||
* for any potentially buffered data.
|
||||
*
|
||||
* Consider using the higher level `send` functions instead.
|
||||
*/
|
||||
open func end() {
|
||||
assert(!writableEnded)
|
||||
|
||||
writableEnded = true
|
||||
let callbacks = _endHandlers; _endHandlers = []
|
||||
|
||||
guard let connection = connection else {
|
||||
return callbacks.forEach { $0(self) }
|
||||
}
|
||||
|
||||
if !didWriteHead { writeHead() }
|
||||
flushIfPossible()
|
||||
|
||||
let message = NWProtocolFramer.Message.httpMessage
|
||||
message.isEndOfMessage = true
|
||||
let context = NWConnection
|
||||
.ContentContext(identifier: "HTTPResponseEnd", metadata: [ message ])
|
||||
|
||||
let keepAlive = self.keepAlive
|
||||
connection.send(content: nil, contentContext: context, isComplete: true,
|
||||
completion: .contentProcessed({ error in
|
||||
callbacks.forEach { $0(self) }
|
||||
if !keepAlive {
|
||||
connection.cancel()
|
||||
}
|
||||
// TODO: still need to teardown the state!
|
||||
self.handleWriteResult(error)
|
||||
self.connection = nil
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* If body content has been buffered due to a connection not being available,
|
||||
* this will try to flush the buffer.
|
||||
*/
|
||||
public func flushIfPossible() {
|
||||
/* Flush buffered content */
|
||||
guard let connection = connection else { return }
|
||||
defer { self.content = nil }
|
||||
guard let content = content, !content.isEmpty else { return }
|
||||
connection.send(content: content,
|
||||
completion: .contentProcessed(handleWriteResult(_:)))
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Error Handling
|
||||
|
||||
private func handleWriteResult(_ error: NWError?) {
|
||||
if let error = error {
|
||||
handleWriteError(error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked by writing functions if a write failed.
|
||||
*
|
||||
* This will cancel the connection and call all `onEnd` callbacks.
|
||||
*/
|
||||
open func handleWriteError(_ error: NWError) {
|
||||
keepAlive = false
|
||||
if !writableEnded { // like `end`, but no flush, EOM etc
|
||||
writableEnded = true
|
||||
let callbacks = _endHandlers; _endHandlers = []
|
||||
callbacks.forEach { $0(self) }
|
||||
}
|
||||
connection?.cancel()
|
||||
connection = nil
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Header Access
|
||||
// This is crazy wrong and expects consistent casing :-)
|
||||
|
||||
/**
|
||||
* Extract the `Content-Type` header, if available.
|
||||
*/
|
||||
var contentType: String? {
|
||||
set {
|
||||
if let v = newValue { headers["Content-Type"] = v }
|
||||
else { headers.removeValue(forKey: "Content-Type") }
|
||||
}
|
||||
get { return headers["Content-Type"] }
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Write Convenience
|
||||
|
||||
/**
|
||||
* Send the `Data` object to the client. Can only be called once.
|
||||
*
|
||||
* This sets the `Content-Length` header to the length of the Data.
|
||||
* If no `Content-Type` header is set, it will add that as
|
||||
* "application/octet-stream".
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* response.send("hello".data(using: .utf8)!)
|
||||
*
|
||||
*/
|
||||
open func send(_ data: Data) {
|
||||
if !didWriteHead {
|
||||
if headers["Content-Length"] == nil {
|
||||
headers["Content-Length"] = "\(data.count)"
|
||||
}
|
||||
if contentType == nil {
|
||||
contentType = "application/octet-stream"
|
||||
}
|
||||
}
|
||||
write(data)
|
||||
end()
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the `String` object as UTF-8 to the client. Can only be called once.
|
||||
*
|
||||
* This sets the `Content-Length` header to the length of the UTF-8
|
||||
* representation of the String.
|
||||
* If no `Content-Type` header is set, it will add that as
|
||||
* "text/plain; charset=UTF-8".
|
||||
*
|
||||
* If the UTF-8 conversion fails, a `StringEncodingError` is thrown.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* try response.send("hello")
|
||||
*
|
||||
*/
|
||||
open func send(_ string: String) throws {
|
||||
if contentType == nil { contentType = "text/plain; charset=UTF-8" }
|
||||
|
||||
if string.isEmpty { return send(Data()) }
|
||||
|
||||
// TODO: scan headers for requested charset
|
||||
guard let data = string.data(using: .utf8) else {
|
||||
throw StringEncodingError(encoding: .utf8)
|
||||
}
|
||||
|
||||
send(data)
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the `String` object as UTF-8 to the client. Can only be called once.
|
||||
*
|
||||
* This sets the `Content-Length` header to the length of the UTF-8
|
||||
* representation of the String.
|
||||
* If no `Content-Type` header is set, it will add that as
|
||||
* "text/plain; charset=UTF-8".
|
||||
*
|
||||
* If the UTF-8 conversion fails, a `StringEncodingError` is thrown.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* try response.send("hello")
|
||||
*
|
||||
*/
|
||||
open func send<S: StringProtocol>(_ string: S) throws {
|
||||
try send(String(string))
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the Encodable object as JSON to the client. Can only be called once.
|
||||
*
|
||||
* This sets the `Content-Length` header to the length of the encoded data.
|
||||
* If no `Content-Type` header is set, it will add that as
|
||||
* "application/json; charset=UTF-8".
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* struct Entry: Codable {
|
||||
* let date : Date
|
||||
* let title : String
|
||||
* let body : String
|
||||
* }
|
||||
* try response.send(Entry(date: Date(), title: "Hello", body: "World")
|
||||
*
|
||||
*/
|
||||
open func sendJSON<T: Encodable>(_ value: T) throws {
|
||||
if contentType == nil { contentType = "application/json; charset=UTF-8" }
|
||||
let data = try JSONEncoder().encode(value)
|
||||
send(data)
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the property list object as JSON to the client. Can only be called
|
||||
* once.
|
||||
*
|
||||
* This sets the `Content-Length` header to the length of the encoded data.
|
||||
* If no `Content-Type` header is set, it will add that as
|
||||
* "application/json; charset=UTF-8".
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* try response.send([ "title": "Hello", "body": "World" ])
|
||||
*
|
||||
*/
|
||||
open func sendJSON(_ value : Any,
|
||||
options : JSONSerialization.WritingOptions = []) throws
|
||||
{
|
||||
if contentType == nil { contentType = "application/json; charset=UTF-8" }
|
||||
let data =
|
||||
try JSONSerialization.data(withJSONObject: value, options: options)
|
||||
send(data)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
//
|
||||
// StringEncodingError.swift
|
||||
// NWHTTPServer
|
||||
//
|
||||
// Copyright © 2020 ZeeZide GmbH. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct StringEncodingError : Swift.Error {
|
||||
public let encoding : String.Encoding
|
||||
}
|
||||
|
|
@ -276,6 +276,7 @@ private extension PresentationGroupCallState {
|
|||
private enum CurrentImpl {
|
||||
case call(OngoingGroupCallContext)
|
||||
case mediaStream(WrappedMediaStreamingContext)
|
||||
case externalMediaStream(ExternalMediaStreamingContext)
|
||||
}
|
||||
|
||||
private extension CurrentImpl {
|
||||
|
|
@ -283,7 +284,7 @@ private extension CurrentImpl {
|
|||
switch self {
|
||||
case let .call(callContext):
|
||||
return callContext.joinPayload
|
||||
case .mediaStream:
|
||||
case .mediaStream, .externalMediaStream:
|
||||
let ssrcId = UInt32.random(in: 0 ..< UInt32(Int32.max - 1))
|
||||
let dict: [String: Any] = [
|
||||
"fingerprints": [] as [Any],
|
||||
|
|
@ -303,7 +304,7 @@ private extension CurrentImpl {
|
|||
switch self {
|
||||
case let .call(callContext):
|
||||
return callContext.networkState
|
||||
case .mediaStream:
|
||||
case .mediaStream, .externalMediaStream:
|
||||
return .single(OngoingGroupCallContext.NetworkState(isConnected: true, isTransitioningFromBroadcastToRtc: false))
|
||||
}
|
||||
}
|
||||
|
|
@ -312,7 +313,7 @@ private extension CurrentImpl {
|
|||
switch self {
|
||||
case let .call(callContext):
|
||||
return callContext.audioLevels
|
||||
case .mediaStream:
|
||||
case .mediaStream, .externalMediaStream:
|
||||
return .single([])
|
||||
}
|
||||
}
|
||||
|
|
@ -321,7 +322,7 @@ private extension CurrentImpl {
|
|||
switch self {
|
||||
case let .call(callContext):
|
||||
return callContext.isMuted
|
||||
case .mediaStream:
|
||||
case .mediaStream, .externalMediaStream:
|
||||
return .single(true)
|
||||
}
|
||||
}
|
||||
|
|
@ -330,7 +331,7 @@ private extension CurrentImpl {
|
|||
switch self {
|
||||
case let .call(callContext):
|
||||
return callContext.isNoiseSuppressionEnabled
|
||||
case .mediaStream:
|
||||
case .mediaStream, .externalMediaStream:
|
||||
return .single(false)
|
||||
}
|
||||
}
|
||||
|
|
@ -339,7 +340,7 @@ private extension CurrentImpl {
|
|||
switch self {
|
||||
case let .call(callContext):
|
||||
callContext.stop()
|
||||
case .mediaStream:
|
||||
case .mediaStream, .externalMediaStream:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
|
@ -348,7 +349,7 @@ private extension CurrentImpl {
|
|||
switch self {
|
||||
case let .call(callContext):
|
||||
callContext.setIsMuted(isMuted)
|
||||
case .mediaStream:
|
||||
case .mediaStream, .externalMediaStream:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
|
@ -357,7 +358,7 @@ private extension CurrentImpl {
|
|||
switch self {
|
||||
case let .call(callContext):
|
||||
callContext.setIsNoiseSuppressionEnabled(isNoiseSuppressionEnabled)
|
||||
case .mediaStream:
|
||||
case .mediaStream, .externalMediaStream:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
|
@ -366,7 +367,7 @@ private extension CurrentImpl {
|
|||
switch self {
|
||||
case let .call(callContext):
|
||||
callContext.requestVideo(capturer)
|
||||
case .mediaStream:
|
||||
case .mediaStream, .externalMediaStream:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
|
@ -375,7 +376,7 @@ private extension CurrentImpl {
|
|||
switch self {
|
||||
case let .call(callContext):
|
||||
callContext.disableVideo()
|
||||
case .mediaStream:
|
||||
case .mediaStream, .externalMediaStream:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
|
@ -384,7 +385,7 @@ private extension CurrentImpl {
|
|||
switch self {
|
||||
case let .call(callContext):
|
||||
callContext.setVolume(ssrc: ssrc, volume: volume)
|
||||
case .mediaStream:
|
||||
case .mediaStream, .externalMediaStream:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
|
@ -393,7 +394,7 @@ private extension CurrentImpl {
|
|||
switch self {
|
||||
case let .call(callContext):
|
||||
callContext.setRequestedVideoChannels(channels)
|
||||
case .mediaStream:
|
||||
case .mediaStream, .externalMediaStream:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
|
@ -402,17 +403,19 @@ private extension CurrentImpl {
|
|||
switch self {
|
||||
case let .call(callContext):
|
||||
callContext.makeIncomingVideoView(endpointId: endpointId, requestClone: requestClone, completion: completion)
|
||||
case .mediaStream:
|
||||
case .mediaStream, .externalMediaStream:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func video(endpointId: String) -> Signal<OngoingGroupCallContext.VideoFrameData, NoError> {
|
||||
func video(endpointId: String) -> Signal<OngoingGroupCallContext.VideoFrameData, NoError>? {
|
||||
switch self {
|
||||
case let .call(callContext):
|
||||
return callContext.video(endpointId: endpointId)
|
||||
case let .mediaStream(mediaStreamContext):
|
||||
return mediaStreamContext.video()
|
||||
case .externalMediaStream:
|
||||
return .never()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -420,7 +423,7 @@ private extension CurrentImpl {
|
|||
switch self {
|
||||
case let .call(callContext):
|
||||
callContext.addExternalAudioData(data: data)
|
||||
case .mediaStream:
|
||||
case .mediaStream, .externalMediaStream:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
|
@ -429,7 +432,7 @@ private extension CurrentImpl {
|
|||
switch self {
|
||||
case let .call(callContext):
|
||||
callContext.getStats(completion: completion)
|
||||
case .mediaStream:
|
||||
case .mediaStream, .externalMediaStream:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
|
@ -438,7 +441,7 @@ private extension CurrentImpl {
|
|||
switch self {
|
||||
case let .call(callContext):
|
||||
callContext.setTone(tone: tone)
|
||||
case .mediaStream:
|
||||
case .mediaStream, .externalMediaStream:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
|
@ -647,6 +650,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||
private var genericCallContext: CurrentImpl?
|
||||
private var currentConnectionMode: OngoingGroupCallContext.ConnectionMode = .none
|
||||
private var didInitializeConnectionMode: Bool = false
|
||||
|
||||
let externalMediaStream = Promise<ExternalMediaStreamingContext>()
|
||||
|
||||
private var screencastCallContext: OngoingGroupCallContext?
|
||||
private var screencastBufferServerContext: IpcGroupCallBufferAppContext?
|
||||
|
|
@ -1638,7 +1643,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||
genericCallContext = current
|
||||
} else {
|
||||
if self.isStream, self.accountContext.sharedContext.immediateExperimentalUISettings.liveStreamV2 {
|
||||
genericCallContext = .mediaStream(WrappedMediaStreamingContext(rejoinNeeded: { [weak self] in
|
||||
let externalMediaStream = ExternalMediaStreamingContext(id: self.internalId, rejoinNeeded: { [weak self] in
|
||||
Queue.mainQueue().async {
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
|
|
@ -1650,7 +1655,9 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||
strongSelf.requestCall(movingFromBroadcastToRtc: false)
|
||||
}
|
||||
}
|
||||
}))
|
||||
})
|
||||
genericCallContext = .externalMediaStream(externalMediaStream)
|
||||
self.externalMediaStream.set(.single(externalMediaStream))
|
||||
} else {
|
||||
var outgoingAudioBitrateKbit: Int32?
|
||||
let appConfiguration = self.accountContext.currentAppConfiguration.with({ $0 })
|
||||
|
|
@ -1797,6 +1804,14 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||
strongSelf.currentConnectionMode = .broadcast
|
||||
mediaStreamContext.setAudioStreamData(audioStreamData: OngoingGroupCallContext.AudioStreamData(engine: strongSelf.accountContext.engine, callId: callInfo.id, accessHash: callInfo.accessHash, isExternalStream: callInfo.isStream))
|
||||
}
|
||||
case let .externalMediaStream(externalMediaStream):
|
||||
switch joinCallResult.connectionMode {
|
||||
case .rtc:
|
||||
strongSelf.currentConnectionMode = .rtc
|
||||
case .broadcast:
|
||||
strongSelf.currentConnectionMode = .broadcast
|
||||
externalMediaStream.setAudioStreamData(audioStreamData: OngoingGroupCallContext.AudioStreamData(engine: strongSelf.accountContext.engine, callId: callInfo.id, accessHash: callInfo.accessHash, isExternalStream: callInfo.isStream))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3199,7 +3214,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||
switch genericCallContext {
|
||||
case let .call(callContext):
|
||||
callContext.setConnectionMode(.none, keepBroadcastConnectedIfWasEnabled: movingFromBroadcastToRtc, isUnifiedBroadcast: false)
|
||||
case .mediaStream:
|
||||
case .mediaStream, .externalMediaStream:
|
||||
assertionFailure()
|
||||
break
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1896,6 +1896,7 @@ public final class AccountViewTracker {
|
|||
addHole = true
|
||||
}
|
||||
pollingCompleted = context.isUpdated.get()
|
||||
//pollingCompleted = .single(true)
|
||||
} else {
|
||||
addHole = true
|
||||
pollingCompleted = .single(true)
|
||||
|
|
|
|||
|
|
@ -236,6 +236,59 @@ private func mergedResult(_ state: SearchMessagesState) -> SearchMessagesResult
|
|||
return SearchMessagesResult(messages: messages, readStates: readStates, threadInfo: threadInfo, totalCount: state.main.totalCount + (state.additional?.totalCount ?? 0), completed: state.main.completed && (state.additional?.completed ?? true))
|
||||
}
|
||||
|
||||
func _internal_getSearchMessageCount(account: Account, location: SearchMessagesLocation, query: String) -> Signal<Int?, NoError> {
|
||||
guard case let .peer(peerId, fromId, _, _, threadId, _, _) = location else {
|
||||
return .single(nil)
|
||||
}
|
||||
return account.postbox.transaction { transaction -> (Api.InputPeer?, Api.InputPeer?) in
|
||||
var chatPeer = transaction.getPeer(peerId).flatMap(apiInputPeer)
|
||||
var fromPeer: Api.InputPeer?
|
||||
if let fromId {
|
||||
if let value = transaction.getPeer(fromId).flatMap(apiInputPeer) {
|
||||
fromPeer = value
|
||||
} else {
|
||||
chatPeer = nil
|
||||
}
|
||||
}
|
||||
|
||||
return (chatPeer, fromPeer)
|
||||
}
|
||||
|> mapToSignal { inputPeer, fromPeer -> Signal<Int?, NoError> in
|
||||
guard let inputPeer else {
|
||||
return .single(nil)
|
||||
}
|
||||
|
||||
var flags: Int32 = 0
|
||||
|
||||
if let _ = fromPeer {
|
||||
flags |= (1 << 0)
|
||||
}
|
||||
|
||||
var topMsgId: Int32?
|
||||
if let threadId = threadId {
|
||||
flags |= (1 << 1)
|
||||
topMsgId = Int32(clamping: threadId)
|
||||
}
|
||||
|
||||
return account.network.request(Api.functions.messages.search(flags: flags, peer: inputPeer, q: query, fromId: fromPeer, savedPeerId: nil, savedReaction: nil, topMsgId: topMsgId, filter: .inputMessagesFilterEmpty, minDate: 0, maxDate: 0, offsetId: 0, addOffset: 0, limit: 1, maxId: 0, minId: 0, hash: 0))
|
||||
|> map { result -> Int? in
|
||||
switch result {
|
||||
case let .channelMessages(_, _, count, _, _, _, _, _):
|
||||
return Int(count)
|
||||
case let .messages(messages, _, _):
|
||||
return messages.count
|
||||
case let .messagesNotModified(count):
|
||||
return Int(count)
|
||||
case let .messagesSlice(_, count, _, _, _, _, _):
|
||||
return Int(count)
|
||||
}
|
||||
}
|
||||
|> `catch` { _ -> Signal<Int?, NoError> in
|
||||
return .single(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func _internal_searchMessages(account: Account, location: SearchMessagesLocation, query: String, state: SearchMessagesState?, centerId: MessageId?, limit: Int32 = 100) -> Signal<(SearchMessagesResult, SearchMessagesState), NoError> {
|
||||
if case let .peer(peerId, fromId, tags, reactions, threadId, minDate, maxDate) = location, fromId == nil, tags == nil, peerId == account.peerId, let reactions, let reaction = reactions.first, (minDate == nil || minDate == 0), (maxDate == nil || maxDate == 0) {
|
||||
return account.postbox.transaction { transaction -> (SearchMessagesResult, SearchMessagesState) in
|
||||
|
|
|
|||
|
|
@ -76,6 +76,10 @@ public extension TelegramEngine {
|
|||
return _internal_searchMessages(account: self.account, location: location, query: query, state: state, centerId: centerId, limit: limit)
|
||||
}
|
||||
|
||||
public func getSearchMessageCount(location: SearchMessagesLocation, query: String) -> Signal<Int?, NoError> {
|
||||
return _internal_getSearchMessageCount(account: self.account, location: location, query: query)
|
||||
}
|
||||
|
||||
public func searchHashtagPosts(hashtag: String, state: SearchMessagesState?, limit: Int32 = 100) -> Signal<(SearchMessagesResult, SearchMessagesState), NoError> {
|
||||
return _internal_searchHashtagPosts(account: self.account, hashtag: hashtag, state: state, limit: limit)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -194,6 +194,7 @@ private final class AdminUserActionsSheetComponent: Component {
|
|||
let chatPeer: EnginePeer
|
||||
let peers: [RenderedChannelParticipant]
|
||||
let messageCount: Int
|
||||
let deleteAllMessageCount: Int?
|
||||
let completion: (AdminUserActionsSheet.Result) -> Void
|
||||
|
||||
init(
|
||||
|
|
@ -201,12 +202,14 @@ private final class AdminUserActionsSheetComponent: Component {
|
|||
chatPeer: EnginePeer,
|
||||
peers: [RenderedChannelParticipant],
|
||||
messageCount: Int,
|
||||
deleteAllMessageCount: Int?,
|
||||
completion: @escaping (AdminUserActionsSheet.Result) -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.chatPeer = chatPeer
|
||||
self.peers = peers
|
||||
self.messageCount = messageCount
|
||||
self.deleteAllMessageCount = deleteAllMessageCount
|
||||
self.completion = completion
|
||||
}
|
||||
|
||||
|
|
@ -223,6 +226,9 @@ private final class AdminUserActionsSheetComponent: Component {
|
|||
if lhs.messageCount != rhs.messageCount {
|
||||
return false
|
||||
}
|
||||
if lhs.deleteAllMessageCount != rhs.deleteAllMessageCount {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
|
|
@ -642,7 +648,7 @@ private final class AdminUserActionsSheetComponent: Component {
|
|||
let sectionId: AnyHashable
|
||||
let selectedPeers: Set<EnginePeer.Id>
|
||||
let isExpanded: Bool
|
||||
let title: String
|
||||
var title: String
|
||||
|
||||
switch section {
|
||||
case .report:
|
||||
|
|
@ -870,7 +876,14 @@ private final class AdminUserActionsSheetComponent: Component {
|
|||
)))
|
||||
}
|
||||
|
||||
let titleString: String = environment.strings.Chat_AdminActionSheet_DeleteTitle(Int32(component.messageCount))
|
||||
var titleString: String = environment.strings.Chat_AdminActionSheet_DeleteTitle(Int32(component.messageCount))
|
||||
|
||||
if let deleteAllMessageCount = component.deleteAllMessageCount {
|
||||
if self.optionDeleteAllSelectedPeers == Set(component.peers.map(\.peer.id)) {
|
||||
titleString = environment.strings.Chat_AdminActionSheet_DeleteTitle(Int32(deleteAllMessageCount))
|
||||
}
|
||||
}
|
||||
|
||||
let titleSize = self.title.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
|
|
@ -884,7 +897,9 @@ private final class AdminUserActionsSheetComponent: Component {
|
|||
if titleView.superview == nil {
|
||||
self.navigationBarContainer.addSubview(titleView)
|
||||
}
|
||||
transition.setFrame(view: titleView, frame: titleFrame)
|
||||
//transition.setPosition(view: titleView, position: titleFrame.center)
|
||||
titleView.center = titleFrame.center
|
||||
titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size)
|
||||
}
|
||||
|
||||
let navigationBackgroundFrame = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: 54.0))
|
||||
|
|
@ -1424,10 +1439,10 @@ public class AdminUserActionsSheet: ViewControllerComponentContainer {
|
|||
|
||||
private var isDismissed: Bool = false
|
||||
|
||||
public init(context: AccountContext, chatPeer: EnginePeer, peers: [RenderedChannelParticipant], messageCount: Int, completion: @escaping (Result) -> Void) {
|
||||
public init(context: AccountContext, chatPeer: EnginePeer, peers: [RenderedChannelParticipant], messageCount: Int, deleteAllMessageCount: Int?, completion: @escaping (Result) -> Void) {
|
||||
self.context = context
|
||||
|
||||
super.init(context: context, component: AdminUserActionsSheetComponent(context: context, chatPeer: chatPeer, peers: peers, messageCount: messageCount, completion: completion), navigationBarAppearance: .none)
|
||||
super.init(context: context, component: AdminUserActionsSheetComponent(context: context, chatPeer: chatPeer, peers: peers, messageCount: messageCount, deleteAllMessageCount: deleteAllMessageCount, completion: completion), navigationBarAppearance: .none)
|
||||
|
||||
self.statusBar.statusBarStyle = .Ignore
|
||||
self.navigationPresentation = .flatModal
|
||||
|
|
|
|||
|
|
@ -4747,25 +4747,31 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
|||
}
|
||||
if let forwardInfoNode = self.forwardInfoNode, forwardInfoNode.frame.contains(location) {
|
||||
if let item = self.item, let forwardInfo = item.message.forwardInfo {
|
||||
let performAction: () -> Void = {
|
||||
let performAction: () -> Void = { [weak forwardInfoNode] in
|
||||
if let sourceMessageId = forwardInfo.sourceMessageId {
|
||||
if let channel = forwardInfo.author as? TelegramChannel, channel.addressName == nil {
|
||||
if case let .broadcast(info) = channel.info, info.flags.contains(.hasDiscussionGroup) {
|
||||
} else if case .member = channel.participationStatus {
|
||||
} else if !item.message.id.peerId.isReplies {
|
||||
item.controllerInteraction.displayMessageTooltip(item.message.id, item.presentationData.strings.Conversation_PrivateChannelTooltip, false, forwardInfoNode, nil)
|
||||
if let forwardInfoNode {
|
||||
item.controllerInteraction.displayMessageTooltip(item.message.id, item.presentationData.strings.Conversation_PrivateChannelTooltip, false, forwardInfoNode, nil)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
item.controllerInteraction.navigateToMessage(item.message.id, sourceMessageId, NavigateToMessageParams(timestamp: nil, quote: nil))
|
||||
if let forwardInfoNode {
|
||||
item.controllerInteraction.navigateToMessage(item.message.id, sourceMessageId, NavigateToMessageParams(timestamp: nil, quote: nil, progress: forwardInfoNode.makeActivate()?()))
|
||||
}
|
||||
} else if let peer = forwardInfo.source ?? forwardInfo.author {
|
||||
item.controllerInteraction.openPeer(EnginePeer(peer), peer is TelegramUser ? .info(nil) : .chat(textInputState: nil, subject: nil, peekData: nil), nil, .default)
|
||||
} else if let _ = forwardInfo.authorSignature {
|
||||
var subRect: CGRect?
|
||||
if let textNode = forwardInfoNode.nameNode {
|
||||
subRect = textNode.frame
|
||||
if let forwardInfoNode {
|
||||
var subRect: CGRect?
|
||||
if let textNode = forwardInfoNode.nameNode {
|
||||
subRect = textNode.frame
|
||||
}
|
||||
item.controllerInteraction.displayMessageTooltip(item.message.id, item.presentationData.strings.Conversation_ForwardAuthorHiddenTooltip, false, forwardInfoNode, subRect)
|
||||
}
|
||||
item.controllerInteraction.displayMessageTooltip(item.message.id, item.presentationData.strings.Conversation_ForwardAuthorHiddenTooltip, false, forwardInfoNode, subRect)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ swift_library(
|
|||
"//submodules/TelegramUI/Components/TextNodeWithEntities",
|
||||
"//submodules/TelegramUI/Components/AnimationCache",
|
||||
"//submodules/TelegramUI/Components/MultiAnimationRenderer",
|
||||
"//submodules/TelegramUI/Components/TextLoadingEffect",
|
||||
"//submodules/AvatarNode",
|
||||
],
|
||||
visibility = [
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ import TelegramPresentationData
|
|||
import LocalizedPeerData
|
||||
import AccountContext
|
||||
import AvatarNode
|
||||
import TextLoadingEffect
|
||||
import SwiftSignalKit
|
||||
|
||||
public enum ChatMessageForwardInfoType: Equatable {
|
||||
case bubble(incoming: Bool)
|
||||
|
|
@ -85,6 +87,10 @@ public class ChatMessageForwardInfoNode: ASDisplayNode {
|
|||
private var highlightColor: UIColor?
|
||||
private var linkHighlightingNode: LinkHighlightingNode?
|
||||
|
||||
private var hasLinkProgress: Bool = false
|
||||
private var linkProgressView: TextLoadingEffectView?
|
||||
private var linkProgressDisposable: Disposable?
|
||||
|
||||
private var previousPeer: Peer?
|
||||
|
||||
public var openPsa: ((String, ASDisplayNode) -> Void)?
|
||||
|
|
@ -93,6 +99,10 @@ public class ChatMessageForwardInfoNode: ASDisplayNode {
|
|||
super.init()
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.linkProgressDisposable?.dispose()
|
||||
}
|
||||
|
||||
public func hasAction(at point: CGPoint) -> Bool {
|
||||
if let infoNode = self.infoNode, infoNode.frame.contains(point) {
|
||||
return true
|
||||
|
|
@ -172,7 +182,6 @@ public class ChatMessageForwardInfoNode: ASDisplayNode {
|
|||
|
||||
if isHighlighted, !initialRects.isEmpty, let highlightColor = self.highlightColor {
|
||||
let rects = initialRects
|
||||
|
||||
let linkHighlightingNode: LinkHighlightingNode
|
||||
if let current = self.linkHighlightingNode {
|
||||
linkHighlightingNode = current
|
||||
|
|
@ -191,6 +200,85 @@ public class ChatMessageForwardInfoNode: ASDisplayNode {
|
|||
}
|
||||
}
|
||||
|
||||
public func makeActivate() -> (() -> Promise<Bool>?)? {
|
||||
return { [weak self] in
|
||||
guard let self else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let promise = Promise<Bool>()
|
||||
self.linkProgressDisposable?.dispose()
|
||||
|
||||
if self.hasLinkProgress {
|
||||
self.hasLinkProgress = false
|
||||
self.updateLinkProgressState()
|
||||
}
|
||||
|
||||
self.linkProgressDisposable = (promise.get() |> deliverOnMainQueue).startStrict(next: { [weak self] value in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if self.hasLinkProgress != value {
|
||||
self.hasLinkProgress = value
|
||||
self.updateLinkProgressState()
|
||||
}
|
||||
})
|
||||
|
||||
return promise
|
||||
}
|
||||
}
|
||||
|
||||
private func updateLinkProgressState() {
|
||||
guard let highlightColor = self.highlightColor else {
|
||||
return
|
||||
}
|
||||
|
||||
if self.hasLinkProgress, let titleNode = self.titleNode, let nameNode = self.nameNode {
|
||||
var initialRects: [CGRect] = []
|
||||
let addRects: (TextNode, CGPoint, CGFloat) -> Void = { textNode, offset, additionalWidth in
|
||||
guard let cachedLayout = textNode.cachedLayout else {
|
||||
return
|
||||
}
|
||||
for rect in cachedLayout.linesRects() {
|
||||
var rect = rect
|
||||
rect.size.width += rect.origin.x + additionalWidth
|
||||
rect.origin.x = 0.0
|
||||
initialRects.append(rect.offsetBy(dx: offset.x, dy: offset.y))
|
||||
}
|
||||
}
|
||||
|
||||
let offsetY: CGFloat = -12.0
|
||||
if let titleNode = self.titleNode {
|
||||
addRects(titleNode, CGPoint(x: titleNode.frame.minX, y: offsetY + titleNode.frame.minY), 0.0)
|
||||
|
||||
if let nameNode = self.nameNode {
|
||||
addRects(nameNode, CGPoint(x: titleNode.frame.minX, y: offsetY + nameNode.frame.minY), nameNode.frame.minX - titleNode.frame.minX)
|
||||
}
|
||||
}
|
||||
|
||||
let linkProgressView: TextLoadingEffectView
|
||||
if let current = self.linkProgressView {
|
||||
linkProgressView = current
|
||||
} else {
|
||||
linkProgressView = TextLoadingEffectView(frame: CGRect())
|
||||
self.linkProgressView = linkProgressView
|
||||
self.view.addSubview(linkProgressView)
|
||||
}
|
||||
linkProgressView.frame = titleNode.frame
|
||||
|
||||
let progressColor: UIColor = highlightColor
|
||||
|
||||
linkProgressView.update(color: progressColor, size: CGRectUnion(titleNode.frame, nameNode.frame).size, rects: initialRects)
|
||||
} else {
|
||||
if let linkProgressView = self.linkProgressView {
|
||||
self.linkProgressView = nil
|
||||
linkProgressView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak linkProgressView] _ in
|
||||
linkProgressView?.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static func asyncLayout(_ maybeNode: ChatMessageForwardInfoNode?) -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ strings: PresentationStrings, _ type: ChatMessageForwardInfoType, _ peer: Peer?, _ authorName: String?, _ psaType: String?, _ storyData: StoryData?, _ constrainedSize: CGSize) -> (CGSize, (CGFloat) -> ChatMessageForwardInfoNode) {
|
||||
let titleNodeLayout = TextNode.asyncLayout(maybeNode?.titleNode)
|
||||
let nameNodeLayout = TextNode.asyncLayout(maybeNode?.nameNode)
|
||||
|
|
|
|||
|
|
@ -370,6 +370,7 @@ public final class ReplyAccessoryPanelNode: AccessoryPanelNode {
|
|||
super.didLoad()
|
||||
|
||||
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
|
||||
self.view.addGestureRecognizer(UILongPressGestureRecognizer(target: self, action: #selector(self.longPressGesture(_:))))
|
||||
}
|
||||
|
||||
override public func animateIn() {
|
||||
|
|
@ -491,9 +492,9 @@ public final class ReplyAccessoryPanelNode: AccessoryPanelNode {
|
|||
}
|
||||
}
|
||||
|
||||
/*@objc func tapGesture(_ recognizer: UITapGestureRecognizer) {
|
||||
if case .ended = recognizer.state {
|
||||
self.interfaceInteraction?.navigateToMessage(self.messageId, false, true, .generic)
|
||||
@objc func longPressGesture(_ recognizer: UILongPressGestureRecognizer) {
|
||||
if case .began = recognizer.state {
|
||||
self.interfaceInteraction?.navigateToMessage(self.messageId, false, true, ChatLoadingMessageSubject.generic)
|
||||
}
|
||||
}*/
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -868,7 +868,7 @@ final class PeerAllowedReactionsScreenComponent: Component {
|
|||
}
|
||||
contentHeight += reactionCountSectionSize.height
|
||||
|
||||
if "".isEmpty {
|
||||
if !"".isEmpty {
|
||||
contentHeight += 32.0
|
||||
|
||||
let paidReactionsSection: ComponentView<Empty>
|
||||
|
|
|
|||
|
|
@ -171,4 +171,34 @@ public final class TextLoadingEffectView: UIView {
|
|||
self.updateAnimations(size: maskFrame.size)
|
||||
}
|
||||
}
|
||||
|
||||
public func update(color: UIColor, size: CGSize, rects: [CGRect]) {
|
||||
let rectsSet: [CGRect] = rects
|
||||
|
||||
let maskFrame = CGRect(origin: CGPoint(), size: size).insetBy(dx: -4.0, dy: -4.0)
|
||||
|
||||
self.maskContentsView.backgroundColor = color.withAlphaComponent(0.1)
|
||||
self.maskBorderContentsView.backgroundColor = color.withAlphaComponent(0.12)
|
||||
|
||||
self.backgroundView.tintColor = color
|
||||
self.borderBackgroundView.tintColor = color
|
||||
|
||||
self.maskContentsView.frame = maskFrame
|
||||
self.maskBorderContentsView.frame = maskFrame
|
||||
|
||||
self.maskHighlightNode.updateRects(rectsSet)
|
||||
self.maskHighlightNode.frame = CGRect(origin: CGPoint(x: -maskFrame.minX, y: -maskFrame.minY), size: CGSize())
|
||||
|
||||
self.maskBorderHighlightNode.updateRects(rectsSet)
|
||||
self.maskBorderHighlightNode.frame = CGRect(origin: CGPoint(x: -maskFrame.minX, y: -maskFrame.minY), size: CGSize())
|
||||
|
||||
if self.size != maskFrame.size {
|
||||
self.size = maskFrame.size
|
||||
|
||||
self.backgroundView.frame = CGRect(origin: CGPoint(x: -self.gradientWidth, y: 0.0), size: CGSize(width: self.gradientWidth, height: maskFrame.height))
|
||||
self.borderBackgroundView.frame = CGRect(origin: CGPoint(x: -self.gradientWidth, y: 0.0), size: CGSize(width: self.gradientWidth, height: maskFrame.height))
|
||||
|
||||
self.updateAnimations(size: maskFrame.size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ extension ChatControllerImpl {
|
|||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.navigateToMessage(from: fromId, to: .id(id, params), forceInCurrentChat: fromId.peerId == id.peerId && !params.forceNew, forceNew: params.forceNew)
|
||||
self.navigateToMessage(from: fromId, to: .id(id, params), forceInCurrentChat: fromId.peerId == id.peerId && !params.forceNew, forceNew: params.forceNew, progress: params.progress)
|
||||
}
|
||||
|
||||
let _ = (self.context.engine.data.get(
|
||||
|
|
@ -77,6 +77,7 @@ extension ChatControllerImpl {
|
|||
animated: Bool = true,
|
||||
completion: (() -> Void)? = nil,
|
||||
customPresentProgress: ((ViewController, Any?) -> Void)? = nil,
|
||||
progress: Promise<Bool>? = nil,
|
||||
statusSubject: ChatLoadingMessageSubject = .generic
|
||||
) {
|
||||
if !self.isNodeLoaded {
|
||||
|
|
@ -160,31 +161,156 @@ extension ChatControllerImpl {
|
|||
guard let self, let peer = peer else {
|
||||
return
|
||||
}
|
||||
if let navigationController = self.effectiveNavigationController {
|
||||
var chatLocation: NavigateToChatControllerParams.Location = .peer(peer)
|
||||
var displayMessageNotFoundToast = false
|
||||
if case let .channel(channel) = peer, channel.flags.contains(.isForum) {
|
||||
if let message = message, let threadId = message.threadId {
|
||||
chatLocation = .replyThread(ChatReplyThreadMessage(peerId: peer.id, threadId: threadId, channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false))
|
||||
|
||||
var quote: ChatControllerSubject.MessageHighlight.Quote?
|
||||
if case let .id(_, params) = messageLocation {
|
||||
quote = params.quote.flatMap { quote in ChatControllerSubject.MessageHighlight.Quote(string: quote.string, offset: quote.offset) }
|
||||
}
|
||||
var progressValue: Promise<Bool>?
|
||||
if let value = progress {
|
||||
progressValue = value
|
||||
} else if case let .id(_, params) = messageLocation {
|
||||
progressValue = params.progress
|
||||
}
|
||||
self.loadingMessage.set(.single(statusSubject) |> delay(0.1, queue: .mainQueue()))
|
||||
|
||||
var chatLocation: NavigateToChatControllerParams.Location = .peer(peer)
|
||||
var preloadChatLocation: ChatLocation = .peer(id: peer.id)
|
||||
var displayMessageNotFoundToast = false
|
||||
if case let .channel(channel) = peer, channel.flags.contains(.isForum) {
|
||||
if let message = message, let threadId = message.threadId {
|
||||
let replyThreadMessage = ChatReplyThreadMessage(peerId: peer.id, threadId: threadId, channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false)
|
||||
chatLocation = .replyThread(replyThreadMessage)
|
||||
preloadChatLocation = .replyThread(message: replyThreadMessage)
|
||||
} else {
|
||||
displayMessageNotFoundToast = true
|
||||
}
|
||||
}
|
||||
|
||||
let searchLocation: ChatHistoryInitialSearchLocation
|
||||
switch messageLocation {
|
||||
case let .id(id, _):
|
||||
if case let .replyThread(message) = chatLocation, id == message.effectiveMessageId {
|
||||
searchLocation = .index(.absoluteLowerBound())
|
||||
} else {
|
||||
searchLocation = .id(id)
|
||||
}
|
||||
case let .index(index):
|
||||
searchLocation = .index(index)
|
||||
case .upperBound:
|
||||
searchLocation = .index(MessageIndex.upperBound(peerId: chatLocation.peerId))
|
||||
}
|
||||
var historyView: Signal<ChatHistoryViewUpdate, NoError>
|
||||
|
||||
let subject: ChatControllerSubject = .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: quote), timecode: nil)
|
||||
|
||||
historyView = preloadedChatHistoryViewForLocation(ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: searchLocation, quote: nil), count: 50, highlight: true), id: 0), context: self.context, chatLocation: preloadChatLocation, subject: subject, chatLocationContextHolder: Atomic<ChatLocationContextHolder?>(value: nil), fixedCombinedReadStates: nil, tag: nil, additionalData: [])
|
||||
|
||||
var signal: Signal<(MessageIndex?, Bool), NoError>
|
||||
signal = historyView
|
||||
|> mapToSignal { historyView -> Signal<(MessageIndex?, Bool), NoError> in
|
||||
switch historyView {
|
||||
case .Loading:
|
||||
return .single((nil, true))
|
||||
case let .HistoryView(view, _, _, _, _, _, _):
|
||||
for entry in view.entries {
|
||||
if entry.message.id == messageLocation.messageId {
|
||||
return .single((entry.message.index, false))
|
||||
}
|
||||
}
|
||||
if case let .index(index) = searchLocation {
|
||||
return .single((index, false))
|
||||
}
|
||||
return .single((nil, false))
|
||||
}
|
||||
}
|
||||
|> take(until: { index in
|
||||
return SignalTakeAction(passthrough: true, complete: !index.1)
|
||||
})
|
||||
|
||||
/*#if DEBUG
|
||||
signal = .single((nil, true)) |> then(signal |> delay(2.0, queue: .mainQueue()))
|
||||
#endif*/
|
||||
|
||||
var cancelImpl: (() -> Void)?
|
||||
let presentationData = self.presentationData
|
||||
let displayTime = CACurrentMediaTime()
|
||||
let progressSignal = Signal<Never, NoError> { [weak self] subscriber in
|
||||
if let progressValue {
|
||||
progressValue.set(.single(true))
|
||||
return ActionDisposable {
|
||||
Queue.mainQueue().async() {
|
||||
progressValue.set(.single(false))
|
||||
}
|
||||
}
|
||||
} else if case .generic = statusSubject {
|
||||
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: {
|
||||
if CACurrentMediaTime() - displayTime > 1.5 {
|
||||
cancelImpl?()
|
||||
}
|
||||
}))
|
||||
if let customPresentProgress = customPresentProgress {
|
||||
customPresentProgress(controller, nil)
|
||||
} else {
|
||||
displayMessageNotFoundToast = true
|
||||
self?.present(controller, in: .window(.root))
|
||||
}
|
||||
return ActionDisposable { [weak controller] in
|
||||
Queue.mainQueue().async() {
|
||||
controller?.dismiss()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return EmptyDisposable
|
||||
}
|
||||
}
|
||||
|> runOn(Queue.mainQueue())
|
||||
|> delay(progressValue == nil ? 0.05 : 0.0, queue: Queue.mainQueue())
|
||||
let progressDisposable = MetaDisposable()
|
||||
var progressStarted = false
|
||||
self.messageIndexDisposable.set((signal
|
||||
|> afterDisposed {
|
||||
Queue.mainQueue().async {
|
||||
progressDisposable.dispose()
|
||||
}
|
||||
}
|
||||
|> deliverOnMainQueue).startStrict(next: { [weak self] index in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
var quote: ChatControllerSubject.MessageHighlight.Quote?
|
||||
if case let .id(_, params) = messageLocation {
|
||||
quote = params.quote.flatMap { quote in ChatControllerSubject.MessageHighlight.Quote(string: quote.string, offset: quote.offset) }
|
||||
if let index = index.0 {
|
||||
let _ = index
|
||||
//strongSelf.chatDisplayNode.historyNode.scrollToMessage(from: scrollFromIndex, to: index, animated: animated, quote: quote, scrollPosition: scrollPosition)
|
||||
} else if index.1 {
|
||||
if !progressStarted {
|
||||
progressStarted = true
|
||||
progressDisposable.set(progressSignal.start())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
let context = self.context
|
||||
self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: chatLocation, subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: quote), timecode: nil), keepStack: .always, chatListCompletion: { chatListController in
|
||||
if displayMessageNotFoundToast {
|
||||
let presentationData = context.sharedContext.currentPresentationData.with({ $0 })
|
||||
chatListController.present(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: presentationData.strings.Conversation_MessageDoesntExist, timeout: nil, customUndoText: nil), elevatedLayout: false, animateInAsReplacement: false, action: { _ in
|
||||
return true
|
||||
}), in: .current)
|
||||
}
|
||||
}))
|
||||
if let navigationController = self.effectiveNavigationController {
|
||||
let context = self.context
|
||||
self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: chatLocation, subject: subject, keepStack: .always, chatListCompletion: { chatListController in
|
||||
if displayMessageNotFoundToast {
|
||||
let presentationData = context.sharedContext.currentPresentationData.with({ $0 })
|
||||
chatListController.present(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: presentationData.strings.Conversation_MessageDoesntExist, timeout: nil, customUndoText: nil), elevatedLayout: false, animateInAsReplacement: false, action: { _ in
|
||||
return true
|
||||
}), in: .current)
|
||||
}
|
||||
}))
|
||||
}
|
||||
}, completed: { [weak self] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.loadingMessage.set(.single(nil))
|
||||
}
|
||||
completion?()
|
||||
}))
|
||||
cancelImpl = { [weak self] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.loadingMessage.set(.single(nil))
|
||||
strongSelf.messageIndexDisposable.set(nil)
|
||||
}
|
||||
}
|
||||
|
||||
completion?()
|
||||
|
|
|
|||
|
|
@ -125,6 +125,14 @@ extension ChatControllerImpl {
|
|||
return
|
||||
}
|
||||
|
||||
var deleteAllMessageCount: Signal<Int?, NoError> = .single(nil)
|
||||
if authors.count == 1 {
|
||||
deleteAllMessageCount = self.context.engine.messages.searchMessages(location: .peer(peerId: peerId, fromId: authors[0].id, tags: nil, reactions: nil, threadId: self.chatLocation.threadId, minDate: nil, maxDate: nil), query: "", state: nil)
|
||||
|> map { result, _ -> Int? in
|
||||
return Int(result.totalCount)
|
||||
}
|
||||
}
|
||||
|
||||
var signal = combineLatest(authors.map { author in
|
||||
self.context.engine.peers.fetchChannelParticipant(peerId: peerId, participantId: author.id)
|
||||
|> map { result -> (Peer, ChannelParticipant?) in
|
||||
|
|
@ -161,8 +169,8 @@ extension ChatControllerImpl {
|
|||
disposables.set(nil)
|
||||
}
|
||||
|
||||
disposables.set((signal
|
||||
|> deliverOnMainQueue).startStrict(next: { [weak self] authorsAndParticipants in
|
||||
disposables.set((combineLatest(signal, deleteAllMessageCount)
|
||||
|> deliverOnMainQueue).startStrict(next: { [weak self] authorsAndParticipants, deleteAllMessageCount in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
|
@ -212,6 +220,7 @@ extension ChatControllerImpl {
|
|||
chatPeer: chatPeer,
|
||||
peers: renderedParticipants,
|
||||
messageCount: messageIds.count,
|
||||
deleteAllMessageCount: deleteAllMessageCount,
|
||||
completion: { [weak self] result in
|
||||
guard let self else {
|
||||
return
|
||||
|
|
@ -259,8 +268,16 @@ extension ChatControllerImpl {
|
|||
disposables.set(nil)
|
||||
}
|
||||
|
||||
disposables.set((signal
|
||||
|> deliverOnMainQueue).startStrict(next: { [weak self] maybeParticipant in
|
||||
var deleteAllMessageCount: Signal<Int?, NoError> = .single(nil)
|
||||
do {
|
||||
deleteAllMessageCount = self.context.engine.messages.getSearchMessageCount(location: .peer(peerId: peerId, fromId: author.id, tags: nil, reactions: nil, threadId: self.chatLocation.threadId, minDate: nil, maxDate: nil), query: "")
|
||||
|> map { result -> Int? in
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
disposables.set((combineLatest(signal, deleteAllMessageCount)
|
||||
|> deliverOnMainQueue).startStrict(next: { [weak self] maybeParticipant, deleteAllMessageCount in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
|
@ -310,6 +327,7 @@ extension ChatControllerImpl {
|
|||
peer: authorPeer._asPeer()
|
||||
)],
|
||||
messageCount: messageIds.count,
|
||||
deleteAllMessageCount: deleteAllMessageCount,
|
||||
completion: { [weak self] result in
|
||||
guard let self else {
|
||||
return
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import WebsiteType
|
|||
import GalleryData
|
||||
import StoryContainerScreen
|
||||
import WallpaperGalleryScreen
|
||||
import FFMpegBinding
|
||||
|
||||
func openChatMessageImpl(_ params: OpenChatMessageParams) -> Bool {
|
||||
var story: TelegramMediaStory?
|
||||
|
|
@ -127,6 +128,24 @@ func openChatMessageImpl(_ params: OpenChatMessageParams) -> Bool {
|
|||
return true
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
for media in params.message.media {
|
||||
if let file = media as? TelegramMediaFile, let fileName = file.fileName, (fileName.hasSuffix(".ts") || fileName.hasSuffix(".mp4s")) {
|
||||
if let path = params.context.account.postbox.mediaBox.completedResourcePath(id: file.resource.id, pathExtension: nil) {
|
||||
let outDir = NSTemporaryDirectory() + "test_remuxed"
|
||||
let _ = try? FileManager.default.removeItem(atPath: outDir)
|
||||
let _ = try? FileManager.default.createDirectory(atPath: outDir, withIntermediateDirectories: true)
|
||||
for i in 0 ... 10 {
|
||||
FFMpegLiveMuxer.remux(path, to: outDir + "/hls_stream0_0000\(i).ts", offsetSeconds: Double(i * 10) - 1.400000)
|
||||
}
|
||||
print("Remuxed into: \(outDir)")
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
if let mediaData = chatMessageGalleryControllerData(context: params.context, chatLocation: params.chatLocation, chatFilterTag: params.chatFilterTag, chatLocationContextHolder: params.chatLocationContextHolder, message: params.message, mediaIndex: params.mediaIndex, navigationController: params.navigationController, standalone: params.standalone, reverseMessageGalleryOrder: params.reverseMessageGalleryOrder, mode: params.mode, source: params.gallerySource, synchronousLoad: false, actionInteraction: params.actionInteraction) {
|
||||
switch mediaData {
|
||||
case let .url(url):
|
||||
|
|
|
|||
|
|
@ -257,10 +257,10 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur
|
|||
if let value = URL(string: "ipfs:/" + parsedUrl.path) {
|
||||
parsedUrl = value
|
||||
}
|
||||
}
|
||||
} else if let scheme = parsedUrl.scheme, scheme == "https", parsedUrl.host == "t.me", parsedUrl.path.hasPrefix("/ipfs/") {
|
||||
if let value = URL(string: "ipfs://" + String(parsedUrl.path[parsedUrl.path.index(parsedUrl.path.startIndex, offsetBy: "/ipfs/".count)...])) {
|
||||
parsedUrl = value
|
||||
} else if parsedUrl.host == "ton" {
|
||||
if let value = URL(string: "ton:/" + parsedUrl.path) {
|
||||
parsedUrl = value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1009,7 +1009,7 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur
|
|||
isInternetUrl = true
|
||||
}
|
||||
if context.sharedContext.immediateExperimentalUISettings.browserExperiment {
|
||||
if parsedUrl.scheme == "ipfs" || parsedUrl.scheme == "ipns" {
|
||||
if parsedUrl.scheme == "ipfs" || parsedUrl.scheme == "ipns" || parsedUrl.scheme == "ton" {
|
||||
isInternetUrl = true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -251,6 +251,10 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent
|
|||
}
|
||||
|
||||
if displayImage {
|
||||
if captureProtected {
|
||||
setLayerDisableScreenshots(self.imageNode.layer, captureProtected)
|
||||
}
|
||||
|
||||
self.imageNode.setSignal(internalMediaGridMessageVideo(postbox: postbox, userLocation: userLocation, videoReference: fileReference, imageReference: imageReference, onlyFullSize: onlyFullSizeThumbnail, useLargeThumbnail: useLargeThumbnail, autoFetchFullSizeThumbnail: autoFetchFullSizeThumbnail || fileReference.media.isInstantVideo) |> map { [weak self] getSize, getData in
|
||||
Queue.mainQueue().async {
|
||||
if let strongSelf = self, strongSelf.dimensions == nil {
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ swift_library(
|
|||
"//submodules/TelegramUIPreferences:TelegramUIPreferences",
|
||||
"//submodules/TgVoip:TgVoip",
|
||||
"//submodules/TgVoipWebrtc:TgVoipWebrtc",
|
||||
"//submodules/FFMpegBinding",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
|
|
|||
|
|
@ -2,6 +2,9 @@ import Foundation
|
|||
import SwiftSignalKit
|
||||
import TgVoipWebrtc
|
||||
import TelegramCore
|
||||
import Network
|
||||
import Postbox
|
||||
import FFMpegBinding
|
||||
|
||||
public final class WrappedMediaStreamingContext {
|
||||
private final class Impl {
|
||||
|
|
@ -132,3 +135,458 @@ public final class WrappedMediaStreamingContext {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
public final class ExternalMediaStreamingContext {
|
||||
private final class Impl {
|
||||
let queue: Queue
|
||||
|
||||
private var broadcastPartsSource: BroadcastPartSource?
|
||||
|
||||
private let resetPlaylistDisposable = MetaDisposable()
|
||||
private let updatePlaylistDisposable = MetaDisposable()
|
||||
|
||||
let masterPlaylistData = Promise<String>()
|
||||
let playlistData = Promise<String>()
|
||||
let mediumPlaylistData = Promise<String>()
|
||||
|
||||
init(queue: Queue, rejoinNeeded: @escaping () -> Void) {
|
||||
self.queue = queue
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.updatePlaylistDisposable.dispose()
|
||||
}
|
||||
|
||||
func setAudioStreamData(audioStreamData: OngoingGroupCallContext.AudioStreamData?) {
|
||||
if let audioStreamData {
|
||||
let broadcastPartsSource = NetworkBroadcastPartSource(queue: self.queue, engine: audioStreamData.engine, callId: audioStreamData.callId, accessHash: audioStreamData.accessHash, isExternalStream: audioStreamData.isExternalStream)
|
||||
self.broadcastPartsSource = broadcastPartsSource
|
||||
|
||||
self.updatePlaylistDisposable.set(nil)
|
||||
|
||||
let queue = self.queue
|
||||
self.resetPlaylistDisposable.set(broadcastPartsSource.requestTime(completion: { [weak self] timestamp in
|
||||
queue.async {
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
let segmentDuration: Int64 = 1000
|
||||
|
||||
var adjustedTimestamp: Int64 = 0
|
||||
if timestamp > 0 {
|
||||
adjustedTimestamp = timestamp / segmentDuration * segmentDuration - 4 * segmentDuration
|
||||
}
|
||||
|
||||
if adjustedTimestamp > 0 {
|
||||
var masterPlaylistData = "#EXTM3U\n" +
|
||||
"#EXT-X-VERSION:6\n" +
|
||||
"#EXT-X-STREAM-INF:BANDWIDTH=3300000,RESOLUTION=1280x720,CODECS=\"avc1.64001f,mp4a.40.2\"\n" +
|
||||
"hls_level_0.m3u8\n"
|
||||
|
||||
masterPlaylistData += "#EXT-X-STREAM-INF:BANDWIDTH=1000000,RESOLUTION=640x360,CODECS=\"avc1.64001f,mp4a.40.2\"\n" +
|
||||
"hls_level_1.m3u8\n"
|
||||
|
||||
self.masterPlaylistData.set(.single(masterPlaylistData))
|
||||
|
||||
self.beginUpdatingPlaylist(initialHeadTimestamp: adjustedTimestamp)
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
private func beginUpdatingPlaylist(initialHeadTimestamp: Int64) {
|
||||
let segmentDuration: Int64 = 1000
|
||||
|
||||
var timestamp = initialHeadTimestamp
|
||||
self.updatePlaylist(headTimestamp: timestamp, quality: 0)
|
||||
self.updatePlaylist(headTimestamp: timestamp, quality: 1)
|
||||
|
||||
self.updatePlaylistDisposable.set((
|
||||
Signal<Void, NoError>.single(Void())
|
||||
|> delay(1.0, queue: self.queue)
|
||||
|> restart
|
||||
|> deliverOn(self.queue)
|
||||
).start(next: { [weak self] _ in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
timestamp += segmentDuration
|
||||
self.updatePlaylist(headTimestamp: timestamp, quality: 0)
|
||||
self.updatePlaylist(headTimestamp: timestamp, quality: 1)
|
||||
}))
|
||||
}
|
||||
|
||||
private func updatePlaylist(headTimestamp: Int64, quality: Int) {
|
||||
let segmentDuration: Int64 = 1000
|
||||
let headIndex = headTimestamp / segmentDuration
|
||||
let minIndex = headIndex - 20
|
||||
|
||||
var playlistData = "#EXTM3U\n" +
|
||||
"#EXT-X-VERSION:6\n" +
|
||||
"#EXT-X-TARGETDURATION:1\n" +
|
||||
"#EXT-X-MEDIA-SEQUENCE:\(minIndex)\n" +
|
||||
"#EXT-X-INDEPENDENT-SEGMENTS\n"
|
||||
|
||||
for index in minIndex ... headIndex {
|
||||
playlistData.append("#EXTINF:1.000000,\n")
|
||||
playlistData.append("hls_stream\(quality)_\(index).ts\n")
|
||||
}
|
||||
|
||||
print("Player: updating playlist \(quality) \(minIndex) ... \(headIndex)")
|
||||
|
||||
if quality == 0 {
|
||||
self.playlistData.set(.single(playlistData))
|
||||
} else {
|
||||
self.mediumPlaylistData.set(.single(playlistData))
|
||||
}
|
||||
}
|
||||
|
||||
func partData(index: Int, quality: Int) -> Signal<Data?, NoError> {
|
||||
let segmentDuration: Int64 = 1000
|
||||
let timestamp = Int64(index) * segmentDuration
|
||||
|
||||
print("Player: request part(q: \(quality)) \(index) -> \(timestamp)")
|
||||
|
||||
guard let broadcastPartsSource = self.broadcastPartsSource else {
|
||||
return .single(nil)
|
||||
}
|
||||
|
||||
return Signal { subscriber in
|
||||
return broadcastPartsSource.requestPart(
|
||||
timestampMilliseconds: timestamp,
|
||||
durationMilliseconds: segmentDuration,
|
||||
subject: .video(channelId: 1, quality: quality == 0 ? .full : .medium),
|
||||
completion: { part in
|
||||
var data = part.oggData
|
||||
if data.count > 32 {
|
||||
data = data.subdata(in: 32 ..< data.count)
|
||||
}
|
||||
subscriber.putNext(data)
|
||||
},
|
||||
rejoinNeeded: {
|
||||
//TODO
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let queue = Queue()
|
||||
let id: CallSessionInternalId
|
||||
private let impl: QueueLocalObject<Impl>
|
||||
private var hlsServerDisposable: Disposable?
|
||||
|
||||
public init(id: CallSessionInternalId, rejoinNeeded: @escaping () -> Void) {
|
||||
self.id = id
|
||||
let queue = self.queue
|
||||
self.impl = QueueLocalObject(queue: queue, generate: {
|
||||
return Impl(queue: queue, rejoinNeeded: rejoinNeeded)
|
||||
})
|
||||
|
||||
self.hlsServerDisposable = SharedHLSServer.shared.registerPlayer(streamingContext: self)
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.hlsServerDisposable?.dispose()
|
||||
}
|
||||
|
||||
public func setAudioStreamData(audioStreamData: OngoingGroupCallContext.AudioStreamData?) {
|
||||
self.impl.with { impl in
|
||||
impl.setAudioStreamData(audioStreamData: audioStreamData)
|
||||
}
|
||||
}
|
||||
|
||||
public func masterPlaylistData() -> Signal<String, NoError> {
|
||||
return self.impl.signalWith { impl, subscriber in
|
||||
impl.masterPlaylistData.get().start(next: subscriber.putNext)
|
||||
}
|
||||
}
|
||||
|
||||
public func playlistData(quality: Int) -> Signal<String, NoError> {
|
||||
return self.impl.signalWith { impl, subscriber in
|
||||
if quality == 0 {
|
||||
impl.playlistData.get().start(next: subscriber.putNext)
|
||||
} else {
|
||||
impl.mediumPlaylistData.get().start(next: subscriber.putNext)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func partData(index: Int, quality: Int) -> Signal<Data?, NoError> {
|
||||
return self.impl.signalWith { impl, subscriber in
|
||||
impl.partData(index: index, quality: quality).start(next: subscriber.putNext)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public final class SharedHLSServer {
|
||||
public static let shared: SharedHLSServer = {
|
||||
return SharedHLSServer()
|
||||
}()
|
||||
|
||||
private enum ResponseError {
|
||||
case badRequest
|
||||
case notFound
|
||||
case internalServerError
|
||||
|
||||
var httpString: String {
|
||||
switch self {
|
||||
case .badRequest:
|
||||
return "400 Bad Request"
|
||||
case .notFound:
|
||||
return "404 Not Found"
|
||||
case .internalServerError:
|
||||
return "500 Internal Server Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class ContextReference {
|
||||
weak var streamingContext: ExternalMediaStreamingContext?
|
||||
|
||||
init(streamingContext: ExternalMediaStreamingContext) {
|
||||
self.streamingContext = streamingContext
|
||||
}
|
||||
}
|
||||
|
||||
private final class Impl {
|
||||
private let queue: Queue
|
||||
|
||||
private let port: NWEndpoint.Port
|
||||
private var listener: NWListener?
|
||||
|
||||
private var contextReferences = Bag<ContextReference>()
|
||||
|
||||
init(queue: Queue, port: UInt16) {
|
||||
self.queue = queue
|
||||
self.port = NWEndpoint.Port(rawValue: port)!
|
||||
self.start()
|
||||
}
|
||||
|
||||
func start() {
|
||||
let listener: NWListener
|
||||
do {
|
||||
listener = try NWListener(using: .tcp, on: self.port)
|
||||
} catch {
|
||||
Logger.shared.log("SharedHLSServer", "Failed to create listener: \(error)")
|
||||
return
|
||||
}
|
||||
self.listener = listener
|
||||
|
||||
listener.newConnectionHandler = { [weak self] connection in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.handleConnection(connection: connection)
|
||||
}
|
||||
|
||||
listener.stateUpdateHandler = { [weak self] state in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
switch state {
|
||||
case .ready:
|
||||
Logger.shared.log("SharedHLSServer", "Server is ready on port \(self.port)")
|
||||
case let .failed(error):
|
||||
Logger.shared.log("SharedHLSServer", "Server failed with error: \(error)")
|
||||
self.listener?.cancel()
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
listener.start(queue: self.queue.queue)
|
||||
}
|
||||
|
||||
private func handleConnection(connection: NWConnection) {
|
||||
connection.start(queue: self.queue.queue)
|
||||
connection.receive(minimumIncompleteLength: 1, maximumLength: 1024, completion: { [weak self] data, _, isComplete, error in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if let data, !data.isEmpty {
|
||||
self.handleRequest(data: data, connection: connection)
|
||||
} else if isComplete {
|
||||
connection.cancel()
|
||||
} else if let error = error {
|
||||
Logger.shared.log("SharedHLSServer", "Error on connection: \(error)")
|
||||
connection.cancel()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private func handleRequest(data: Data, connection: NWConnection) {
|
||||
guard let requestString = String(data: data, encoding: .utf8) else {
|
||||
connection.cancel()
|
||||
return
|
||||
}
|
||||
|
||||
if !requestString.hasPrefix("GET /") {
|
||||
self.sendErrorAndClose(connection: connection)
|
||||
return
|
||||
}
|
||||
guard let firstCrLf = requestString.range(of: "\r\n") else {
|
||||
self.sendErrorAndClose(connection: connection)
|
||||
return
|
||||
}
|
||||
let firstLine = String(requestString[requestString.index(requestString.startIndex, offsetBy: "GET /".count) ..< firstCrLf.lowerBound])
|
||||
if !(firstLine.hasSuffix(" HTTP/1.0") || firstLine.hasSuffix(" HTTP/1.1")) {
|
||||
self.sendErrorAndClose(connection: connection)
|
||||
return
|
||||
}
|
||||
|
||||
let requestPath = String(firstLine[firstLine.startIndex ..< firstLine.index(firstLine.endIndex, offsetBy: -" HTTP/1.1".count)])
|
||||
|
||||
guard let firstSlash = requestPath.range(of: "/") else {
|
||||
self.sendErrorAndClose(connection: connection, error: .notFound)
|
||||
return
|
||||
}
|
||||
guard let streamId = UUID(uuidString: String(requestPath[requestPath.startIndex ..< firstSlash.lowerBound])) else {
|
||||
self.sendErrorAndClose(connection: connection)
|
||||
return
|
||||
}
|
||||
guard let streamingContext = self.contextReferences.copyItems().first(where: { $0.streamingContext?.id == streamId })?.streamingContext else {
|
||||
self.sendErrorAndClose(connection: connection)
|
||||
return
|
||||
}
|
||||
|
||||
let filePath = String(requestPath[firstSlash.upperBound...])
|
||||
if filePath == "master.m3u8" {
|
||||
let _ = (streamingContext.masterPlaylistData()
|
||||
|> deliverOn(self.queue)
|
||||
|> take(1)).start(next: { [weak self] result in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
self.sendResponseAndClose(connection: connection, data: result.data(using: .utf8)!)
|
||||
})
|
||||
} 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(connection: connection)
|
||||
return
|
||||
}
|
||||
|
||||
let _ = (streamingContext.playlistData(quality: levelIndex)
|
||||
|> deliverOn(self.queue)
|
||||
|> take(1)).start(next: { [weak self] result in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
self.sendResponseAndClose(connection: connection, data: result.data(using: .utf8)!)
|
||||
})
|
||||
} else if filePath.hasPrefix("hls_stream") && filePath.hasSuffix(".ts") {
|
||||
let fileId = String(filePath[filePath.index(filePath.startIndex, offsetBy: "hls_stream".count) ..< filePath.index(filePath.endIndex, offsetBy: -".ts".count)])
|
||||
guard let underscoreRange = fileId.range(of: "_") else {
|
||||
self.sendErrorAndClose(connection: connection)
|
||||
return
|
||||
}
|
||||
guard let levelIndex = Int(String(fileId[fileId.startIndex ..< underscoreRange.lowerBound])) else {
|
||||
self.sendErrorAndClose(connection: connection)
|
||||
return
|
||||
}
|
||||
guard let partIndex = Int(String(fileId[underscoreRange.upperBound...])) else {
|
||||
self.sendErrorAndClose(connection: connection)
|
||||
return
|
||||
}
|
||||
let _ = (streamingContext.partData(index: partIndex, quality: levelIndex)
|
||||
|> deliverOn(self.queue)
|
||||
|> take(1)).start(next: { [weak self] result in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
if let result {
|
||||
let sourceTempFile = TempBox.shared.tempFile(fileName: "part.mp4")
|
||||
let tempFile = TempBox.shared.tempFile(fileName: "part.ts")
|
||||
defer {
|
||||
TempBox.shared.dispose(sourceTempFile)
|
||||
TempBox.shared.dispose(tempFile)
|
||||
}
|
||||
|
||||
guard let _ = try? result.write(to: URL(fileURLWithPath: sourceTempFile.path)) else {
|
||||
self.sendErrorAndClose(connection: connection, error: .internalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
let sourcePath = sourceTempFile.path
|
||||
FFMpegLiveMuxer.remux(sourcePath, to: tempFile.path, offsetSeconds: Double(partIndex))
|
||||
|
||||
if let data = try? Data(contentsOf: URL(fileURLWithPath: tempFile.path)) {
|
||||
self.sendResponseAndClose(connection: connection, data: data)
|
||||
} else {
|
||||
self.sendErrorAndClose(connection: connection, error: .internalServerError)
|
||||
}
|
||||
} else {
|
||||
self.sendErrorAndClose(connection: connection, error: .notFound)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
self.sendErrorAndClose(connection: connection, error: .notFound)
|
||||
}
|
||||
}
|
||||
|
||||
private func sendErrorAndClose(connection: NWConnection, error: ResponseError = .badRequest) {
|
||||
let errorResponse = "HTTP/1.1 \(error.httpString)\r\nContent-Type: text/html\r\nConnection: close\r\n\r\n"
|
||||
connection.send(content: errorResponse.data(using: .utf8), completion: .contentProcessed { error in
|
||||
if let error {
|
||||
Logger.shared.log("SharedHLSServer", "Failed to send response: \(error)")
|
||||
}
|
||||
connection.cancel()
|
||||
})
|
||||
}
|
||||
|
||||
private func sendResponseAndClose(connection: NWConnection, data: Data) {
|
||||
let responseHeaders = "HTTP/1.1 200 OK\r\nContent-Type: application/octet-stream\r\nConnection: close\r\n\r\n"
|
||||
var responseData = Data()
|
||||
responseData.append(responseHeaders.data(using: .utf8)!)
|
||||
responseData.append(data)
|
||||
connection.send(content: responseData, completion: .contentProcessed { error in
|
||||
if let error {
|
||||
Logger.shared.log("SharedHLSServer", "Failed to send response: \(error)")
|
||||
}
|
||||
connection.cancel()
|
||||
})
|
||||
}
|
||||
|
||||
func registerPlayer(streamingContext: ExternalMediaStreamingContext) -> Disposable {
|
||||
let queue = self.queue
|
||||
let index = self.contextReferences.add(ContextReference(streamingContext: streamingContext))
|
||||
|
||||
return ActionDisposable { [weak self] in
|
||||
queue.async {
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.contextReferences.remove(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static let queue = Queue(name: "SharedHLSServer")
|
||||
public let port: UInt16 = 8016
|
||||
private let impl: QueueLocalObject<Impl>
|
||||
|
||||
private init() {
|
||||
let queue = SharedHLSServer.queue
|
||||
let port = self.port
|
||||
self.impl = QueueLocalObject(queue: queue, generate: {
|
||||
return Impl(queue: queue, port: port)
|
||||
})
|
||||
}
|
||||
|
||||
fileprivate func registerPlayer(streamingContext: ExternalMediaStreamingContext) -> Disposable {
|
||||
let disposable = MetaDisposable()
|
||||
|
||||
self.impl.with { impl in
|
||||
disposable.set(impl.registerPlayer(streamingContext: streamingContext))
|
||||
}
|
||||
|
||||
return disposable
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -134,6 +134,9 @@ public func parseInternalUrl(sharedContext: SharedAccountContext, query: String)
|
|||
if query.hasPrefix("ipfs/") {
|
||||
return .externalUrl(url: "ipfs://" + String(query[query.index(query.startIndex, offsetBy: "ipfs/".count)...]))
|
||||
}
|
||||
if query.hasPrefix("ton/") {
|
||||
return .externalUrl(url: "ton://" + String(query[query.index(query.startIndex, offsetBy: "ton/".count)...]))
|
||||
}
|
||||
}
|
||||
|
||||
if pathComponents[0].hasPrefix("+") || pathComponents[0].hasPrefix("%20") {
|
||||
|
|
|
|||
|
|
@ -47,13 +47,13 @@ CONFIGURE_FLAGS="--enable-cross-compile --disable-programs \
|
|||
--enable-libopus \
|
||||
--enable-libvpx \
|
||||
--enable-audiotoolbox \
|
||||
--enable-bsf=aac_adtstoasc,vp9_superframe \
|
||||
--enable-bsf=aac_adtstoasc,vp9_superframe,h264_mp4toannexb \
|
||||
--enable-decoder=h264,libvpx_vp9,hevc,libopus,mp3,aac,flac,alac_at,pcm_s16le,pcm_s24le,gsm_ms_at \
|
||||
--enable-encoder=libvpx_vp9 \
|
||||
--enable-demuxer=aac,mov,m4v,mp3,ogg,libopus,flac,wav,aiff,matroska \
|
||||
--enable-encoder=libvpx_vp9,aac_at \
|
||||
--enable-demuxer=aac,mov,m4v,mp3,ogg,libopus,flac,wav,aiff,matroska,mpegts \
|
||||
--enable-parser=aac,h264,mp3,libopus \
|
||||
--enable-protocol=file \
|
||||
--enable-muxer=mp4,matroska \
|
||||
--enable-muxer=mp4,matroska,mpegts \
|
||||
"
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue