mirror of
https://github.com/yattee/yattee.git
synced 2024-12-22 13:33:42 +00:00
Playback rate
This commit is contained in:
parent
c40fc3e042
commit
bb19fca073
@ -8,13 +8,15 @@ struct PlayerViewController: UIViewControllerRepresentable {
|
|||||||
|
|
||||||
@ObservedObject private var state: PlayerState
|
@ObservedObject private var state: PlayerState
|
||||||
|
|
||||||
|
@ObservedObject private var profile = Profile()
|
||||||
|
|
||||||
var video: Video
|
var video: Video
|
||||||
|
|
||||||
init(video: Video) {
|
init(video: Video) {
|
||||||
self.video = video
|
self.video = video
|
||||||
state = PlayerState(video)
|
state = PlayerState(video)
|
||||||
|
|
||||||
loadStream(video.defaultStream, loadBest: true)
|
loadStream(video.defaultStreamForProfile(profile), loadBest: profile.defaultStreamResolution == .hd720pFirstThenBest)
|
||||||
}
|
}
|
||||||
|
|
||||||
fileprivate func loadStream(_ stream: Stream?, loadBest: Bool = false) {
|
fileprivate func loadStream(_ stream: Stream?, loadBest: Bool = false) {
|
||||||
@ -93,6 +95,7 @@ struct PlayerViewController: UIViewControllerRepresentable {
|
|||||||
items.append(actionsMenu)
|
items.append(actionsMenu)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
items.append(playbackRateMenu)
|
||||||
items.append(streamingQualityMenu)
|
items.append(streamingQualityMenu)
|
||||||
|
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
@ -150,4 +153,28 @@ struct PlayerViewController: UIViewControllerRepresentable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var playbackRateMenu: UIMenu {
|
||||||
|
UIMenu(title: "Playback rate", image: UIImage(systemName: playbackRateMenuImageSystemName), children: playbackRateMenuActions)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var playbackRateMenuImageSystemName: String {
|
||||||
|
if [0.0, 1.0].contains(state.player.rate) {
|
||||||
|
return "speedometer"
|
||||||
|
}
|
||||||
|
|
||||||
|
return state.player.rate < 1.0 ? "tortoise.fill" : "hare.fill"
|
||||||
|
}
|
||||||
|
|
||||||
|
private var playbackRateMenuActions: [UIAction] {
|
||||||
|
PlayerState.availablePlaybackRates.map { rate in
|
||||||
|
let image = state.currentRate == Float(rate) ? UIImage(systemName: "checkmark") : nil
|
||||||
|
|
||||||
|
return UIAction(title: "\(rate)x", image: image) { _ in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
state.setPlayerRate(Float(rate))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,8 +12,6 @@ final class PlayerState: ObservableObject {
|
|||||||
@Published private(set) var composition = AVMutableComposition()
|
@Published private(set) var composition = AVMutableComposition()
|
||||||
@Published private(set) var nextComposition = AVMutableComposition()
|
@Published private(set) var nextComposition = AVMutableComposition()
|
||||||
|
|
||||||
private var comp: AVMutableComposition?
|
|
||||||
|
|
||||||
@Published private(set) var currentStream: Stream!
|
@Published private(set) var currentStream: Stream!
|
||||||
|
|
||||||
@Published private(set) var nextStream: Stream!
|
@Published private(set) var nextStream: Stream!
|
||||||
@ -24,6 +22,11 @@ final class PlayerState: ObservableObject {
|
|||||||
|
|
||||||
@Published var currentSegment: Segment?
|
@Published var currentSegment: Segment?
|
||||||
|
|
||||||
|
private var profile = Profile()
|
||||||
|
|
||||||
|
@Published private(set) var currentRate: Float = 0.0
|
||||||
|
static let availablePlaybackRates: [Double] = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2]
|
||||||
|
|
||||||
var playerItem: AVPlayerItem {
|
var playerItem: AVPlayerItem {
|
||||||
let playerItem = AVPlayerItem(asset: composition)
|
let playerItem = AVPlayerItem(asset: composition)
|
||||||
|
|
||||||
@ -115,6 +118,8 @@ final class PlayerState: ObservableObject {
|
|||||||
player.replaceCurrentItem(with: playerItem)
|
player.replaceCurrentItem(with: playerItem)
|
||||||
streamDidLoad(stream)
|
streamDidLoad(stream)
|
||||||
|
|
||||||
|
player.play()
|
||||||
|
|
||||||
seekToSavedTime()
|
seekToSavedTime()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -179,10 +184,9 @@ final class PlayerState: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let time = savedTime {
|
if let time = savedTime {
|
||||||
|
logger.info("seeking to \(time.seconds)")
|
||||||
player.seek(to: time)
|
player.seek(to: time)
|
||||||
}
|
}
|
||||||
|
|
||||||
player.play()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func destroyPlayer() {
|
func destroyPlayer() {
|
||||||
@ -205,7 +209,22 @@ final class PlayerState: ObservableObject {
|
|||||||
let interval = CMTime(value: 1, timescale: 1)
|
let interval = CMTime(value: 1, timescale: 1)
|
||||||
timeObserver = player.addPeriodicTimeObserver(forInterval: interval, queue: .main) { time in
|
timeObserver = player.addPeriodicTimeObserver(forInterval: interval, queue: .main) { time in
|
||||||
self.currentTime = time
|
self.currentTime = time
|
||||||
self.currentSegment = self.segmentsProvider.segments.first { $0.timeInSegment(time) }
|
|
||||||
|
let currentSegment = self.segmentsProvider.segments.first { $0.timeInSegment(time) }
|
||||||
|
|
||||||
|
if let segment = currentSegment {
|
||||||
|
if self.profile.skippedSegmentsCategories.contains(segment.category) {
|
||||||
|
if segment.shouldSkip(self.currentTime!) {
|
||||||
|
self.player.seek(to: segment.skipTo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.player.rate != self.currentRate, self.player.rate != 0, self.currentRate != 0 {
|
||||||
|
self.player.rate = self.currentRate
|
||||||
|
}
|
||||||
|
|
||||||
|
self.currentSegment = currentSegment
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -218,4 +237,9 @@ final class PlayerState: ObservableObject {
|
|||||||
|
|
||||||
return item.copy() as! AVMetadataItem
|
return item.copy() as! AVMetadataItem
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setPlayerRate(_ rate: Float) {
|
||||||
|
currentRate = rate
|
||||||
|
player.rate = rate
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
23
Model/Profile.swift
Normal file
23
Model/Profile.swift
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
final class Profile: ObservableObject {
|
||||||
|
let defaultStreamResolution: DefaultStreamResolution = .hd720pFirstThenBest
|
||||||
|
|
||||||
|
let skippedSegmentsCategories = [String]() // SponsorBlockSegmentsProvider.categories
|
||||||
|
|
||||||
|
// let sid = "B3_WzklziGu8JKefihLrCsTNavdj73KMiPUBfN5HW2M="
|
||||||
|
let sid = "RpoS7YPPK2-QS81jJF9z4KSQAjmzsOnMpn84c73-GQ8="
|
||||||
|
}
|
||||||
|
|
||||||
|
enum DefaultStreamResolution: String {
|
||||||
|
case hd720pFirstThenBest, hd1080p, hd720p, sd480p, sd360p, sd240p, sd144p
|
||||||
|
|
||||||
|
var value: StreamResolution {
|
||||||
|
switch self {
|
||||||
|
case .hd720pFirstThenBest:
|
||||||
|
return .hd720p
|
||||||
|
default:
|
||||||
|
return StreamResolution(rawValue: rawValue)!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -47,4 +47,8 @@ class Segment: ObservableObject, Hashable {
|
|||||||
func title() -> String {
|
func title() -> String {
|
||||||
category
|
category
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func shouldSkip(_ atTime: CMTime) -> Bool {
|
||||||
|
atTime.seconds - start < 2 && end - atTime.seconds > 2
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@ import Foundation
|
|||||||
import SwiftyJSON
|
import SwiftyJSON
|
||||||
|
|
||||||
final class SponsorBlockSegmentsProvider: ObservableObject {
|
final class SponsorBlockSegmentsProvider: ObservableObject {
|
||||||
let categories = ["sponsor", "selfpromo", "outro", "intro", "music_offtopic", "interaction"]
|
static let categories = ["sponsor", "selfpromo", "outro", "intro", "music_offtopic", "interaction"]
|
||||||
|
|
||||||
@Published var video: Video?
|
@Published var video: Video?
|
||||||
|
|
||||||
@ -29,7 +29,7 @@ final class SponsorBlockSegmentsProvider: ObservableObject {
|
|||||||
private var parameters: [String: String] {
|
private var parameters: [String: String] {
|
||||||
[
|
[
|
||||||
"videoID": id,
|
"videoID": id,
|
||||||
"categories": JSON(categories).rawString(String.Encoding.utf8)!,
|
"categories": JSON(SponsorBlockSegmentsProvider.categories).rawString(String.Encoding.utf8)!,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
enum StreamResolution: String, CaseIterable, Comparable {
|
enum StreamResolution: String, CaseIterable, Comparable {
|
||||||
case hd_1080p, hd_720p, sd_480p, sd_360p, sd_240p, sd_144p
|
case hd1080p, hd720p, sd480p, sd360p, sd240p, sd144p
|
||||||
|
|
||||||
var height: Int {
|
var height: Int {
|
||||||
Int(rawValue.components(separatedBy: CharacterSet.decimalDigits.inverted).joined())!
|
Int(rawValue.components(separatedBy: CharacterSet.decimalDigits.inverted).joined())!
|
||||||
|
@ -5,10 +5,10 @@ import SwiftyJSON
|
|||||||
final class SubscriptionVideosProvider: DataProvider {
|
final class SubscriptionVideosProvider: DataProvider {
|
||||||
@Published var videos = [Video]()
|
@Published var videos = [Video]()
|
||||||
|
|
||||||
var sid: String = "RpoS7YPPK2-QS81jJF9z4KSQAjmzsOnMpn84c73-GQ8="
|
let profile = Profile()
|
||||||
|
|
||||||
func load() {
|
func load() {
|
||||||
let headers = HTTPHeaders([HTTPHeader(name: "Cookie", value: "SID=\(sid)")])
|
let headers = HTTPHeaders([HTTPHeader(name: "Cookie", value: "SID=\(profile.sid)")])
|
||||||
DataProvider.request("auth/feed", headers: headers).responseJSON { response in
|
DataProvider.request("auth/feed", headers: headers).responseJSON { response in
|
||||||
switch response.result {
|
switch response.result {
|
||||||
case let .success(value):
|
case let .success(value):
|
||||||
|
@ -88,6 +88,14 @@ final class Video: Identifiable, ObservableObject {
|
|||||||
selectableStreams.min { $0.resolution > $1.resolution }
|
selectableStreams.min { $0.resolution > $1.resolution }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func streamWithResolution(_ resolution: StreamResolution) -> Stream? {
|
||||||
|
selectableStreams.first { $0.resolution == resolution }
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultStreamForProfile(_ profile: Profile) -> Stream? {
|
||||||
|
streamWithResolution(profile.defaultStreamResolution.value)
|
||||||
|
}
|
||||||
|
|
||||||
private func extractThumbnailURL(from details: JSON) -> URL? {
|
private func extractThumbnailURL(from details: JSON) -> URL? {
|
||||||
if details["videoThumbnails"].arrayValue.isEmpty {
|
if details["videoThumbnails"].arrayValue.isEmpty {
|
||||||
return nil
|
return nil
|
||||||
|
@ -88,6 +88,9 @@
|
|||||||
37C7A1D8267CACE10010EAD6 /* TrendingCategorySelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3705B17D267B4DDE00704544 /* TrendingCategorySelectionView.swift */; };
|
37C7A1D8267CACE10010EAD6 /* TrendingCategorySelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3705B17D267B4DDE00704544 /* TrendingCategorySelectionView.swift */; };
|
||||||
37C7A1D9267CACE60010EAD6 /* VisualEffectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3705B17B267B4D9A00704544 /* VisualEffectView.swift */; };
|
37C7A1D9267CACE60010EAD6 /* VisualEffectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3705B17B267B4D9A00704544 /* VisualEffectView.swift */; };
|
||||||
37C7A1DA267CACF50010EAD6 /* TrendingCountrySelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3705B17F267B4DFB00704544 /* TrendingCountrySelectionView.swift */; };
|
37C7A1DA267CACF50010EAD6 /* TrendingCountrySelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3705B17F267B4DFB00704544 /* TrendingCountrySelectionView.swift */; };
|
||||||
|
37C7A1DC267CE9D90010EAD6 /* Profile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C7A1DB267CE9D90010EAD6 /* Profile.swift */; };
|
||||||
|
37C7A1DD267CE9D90010EAD6 /* Profile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C7A1DB267CE9D90010EAD6 /* Profile.swift */; };
|
||||||
|
37C7A1DE267CE9D90010EAD6 /* Profile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C7A1DB267CE9D90010EAD6 /* Profile.swift */; };
|
||||||
37C7A9042679059200E721B4 /* AVKeyValueStatus+String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C7A9032679059200E721B4 /* AVKeyValueStatus+String.swift */; };
|
37C7A9042679059200E721B4 /* AVKeyValueStatus+String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C7A9032679059200E721B4 /* AVKeyValueStatus+String.swift */; };
|
||||||
37C7A905267905AE00E721B4 /* AVKeyValueStatus+String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C7A9032679059200E721B4 /* AVKeyValueStatus+String.swift */; };
|
37C7A905267905AE00E721B4 /* AVKeyValueStatus+String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C7A9032679059200E721B4 /* AVKeyValueStatus+String.swift */; };
|
||||||
37C7A906267905AF00E721B4 /* AVKeyValueStatus+String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C7A9032679059200E721B4 /* AVKeyValueStatus+String.swift */; };
|
37C7A906267905AF00E721B4 /* AVKeyValueStatus+String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C7A9032679059200E721B4 /* AVKeyValueStatus+String.swift */; };
|
||||||
@ -190,6 +193,7 @@
|
|||||||
37AAF29F26741C97007FC770 /* SubscriptionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionsView.swift; sourceTree = "<group>"; };
|
37AAF29F26741C97007FC770 /* SubscriptionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionsView.swift; sourceTree = "<group>"; };
|
||||||
37B767DA2677C3CA0098BAA8 /* PlayerState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerState.swift; sourceTree = "<group>"; };
|
37B767DA2677C3CA0098BAA8 /* PlayerState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerState.swift; sourceTree = "<group>"; };
|
||||||
37C7A1D4267BFD9D0010EAD6 /* SponsorBlockSegment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SponsorBlockSegment.swift; sourceTree = "<group>"; };
|
37C7A1D4267BFD9D0010EAD6 /* SponsorBlockSegment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SponsorBlockSegment.swift; sourceTree = "<group>"; };
|
||||||
|
37C7A1DB267CE9D90010EAD6 /* Profile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Profile.swift; sourceTree = "<group>"; };
|
||||||
37C7A9032679059200E721B4 /* AVKeyValueStatus+String.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AVKeyValueStatus+String.swift"; sourceTree = "<group>"; };
|
37C7A9032679059200E721B4 /* AVKeyValueStatus+String.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AVKeyValueStatus+String.swift"; sourceTree = "<group>"; };
|
||||||
37CEE4B42677B628005A1EFE /* StreamType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StreamType.swift; sourceTree = "<group>"; };
|
37CEE4B42677B628005A1EFE /* StreamType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StreamType.swift; sourceTree = "<group>"; };
|
||||||
37CEE4B82677B63F005A1EFE /* StreamResolution.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StreamResolution.swift; sourceTree = "<group>"; };
|
37CEE4B82677B63F005A1EFE /* StreamResolution.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StreamResolution.swift; sourceTree = "<group>"; };
|
||||||
@ -390,6 +394,7 @@
|
|||||||
37D4B1AF2672A01000C925CA /* DataProvider.swift */,
|
37D4B1AF2672A01000C925CA /* DataProvider.swift */,
|
||||||
37B767DA2677C3CA0098BAA8 /* PlayerState.swift */,
|
37B767DA2677C3CA0098BAA8 /* PlayerState.swift */,
|
||||||
37D4B19226717CE100C925CA /* PopularVideosProvider.swift */,
|
37D4B19226717CE100C925CA /* PopularVideosProvider.swift */,
|
||||||
|
37C7A1DB267CE9D90010EAD6 /* Profile.swift */,
|
||||||
37AAF2812673791F007FC770 /* SearchedVideosProvider.swift */,
|
37AAF2812673791F007FC770 /* SearchedVideosProvider.swift */,
|
||||||
37EAD86E267B9ED100D9E01B /* Segment.swift */,
|
37EAD86E267B9ED100D9E01B /* Segment.swift */,
|
||||||
37C7A1D4267BFD9D0010EAD6 /* SponsorBlockSegment.swift */,
|
37C7A1D4267BFD9D0010EAD6 /* SponsorBlockSegment.swift */,
|
||||||
@ -669,6 +674,7 @@
|
|||||||
3705B182267B4E4900704544 /* TrendingCategory.swift in Sources */,
|
3705B182267B4E4900704544 /* TrendingCategory.swift in Sources */,
|
||||||
37EAD86F267B9ED100D9E01B /* Segment.swift in Sources */,
|
37EAD86F267B9ED100D9E01B /* Segment.swift in Sources */,
|
||||||
37CEE4B52677B628005A1EFE /* StreamType.swift in Sources */,
|
37CEE4B52677B628005A1EFE /* StreamType.swift in Sources */,
|
||||||
|
37C7A1DC267CE9D90010EAD6 /* Profile.swift in Sources */,
|
||||||
3714166F267A8ACC006CA35D /* TrendingView.swift in Sources */,
|
3714166F267A8ACC006CA35D /* TrendingView.swift in Sources */,
|
||||||
377FC7E3267A084A00A6BBAF /* VideoThumbnailView.swift in Sources */,
|
377FC7E3267A084A00A6BBAF /* VideoThumbnailView.swift in Sources */,
|
||||||
37AAF2822673791F007FC770 /* SearchedVideosProvider.swift in Sources */,
|
37AAF2822673791F007FC770 /* SearchedVideosProvider.swift in Sources */,
|
||||||
@ -723,6 +729,7 @@
|
|||||||
377FC7E4267A084E00A6BBAF /* SearchView.swift in Sources */,
|
377FC7E4267A084E00A6BBAF /* SearchView.swift in Sources */,
|
||||||
377FC7E0267A082600A6BBAF /* ChannelView.swift in Sources */,
|
377FC7E0267A082600A6BBAF /* ChannelView.swift in Sources */,
|
||||||
37C7A1D6267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */,
|
37C7A1D6267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */,
|
||||||
|
37C7A1DD267CE9D90010EAD6 /* Profile.swift in Sources */,
|
||||||
37C7A906267905AF00E721B4 /* AVKeyValueStatus+String.swift in Sources */,
|
37C7A906267905AF00E721B4 /* AVKeyValueStatus+String.swift in Sources */,
|
||||||
37B767DC2677C3CA0098BAA8 /* PlayerState.swift in Sources */,
|
37B767DC2677C3CA0098BAA8 /* PlayerState.swift in Sources */,
|
||||||
37D4B1B52672A30700C925CA /* VideoDetailsProvider.swift in Sources */,
|
37D4B1B52672A30700C925CA /* VideoDetailsProvider.swift in Sources */,
|
||||||
@ -772,6 +779,7 @@
|
|||||||
37AAF29226740715007FC770 /* AppState.swift in Sources */,
|
37AAF29226740715007FC770 /* AppState.swift in Sources */,
|
||||||
37EAD86D267B9C5600D9E01B /* SponsorBlockSegmentsProvider.swift in Sources */,
|
37EAD86D267B9C5600D9E01B /* SponsorBlockSegmentsProvider.swift in Sources */,
|
||||||
3705B17C267B4D9A00704544 /* VisualEffectView.swift in Sources */,
|
3705B17C267B4D9A00704544 /* VisualEffectView.swift in Sources */,
|
||||||
|
37C7A1DE267CE9D90010EAD6 /* Profile.swift in Sources */,
|
||||||
3741B5302676213400125C5E /* PlayerViewController.swift in Sources */,
|
3741B5302676213400125C5E /* PlayerViewController.swift in Sources */,
|
||||||
37B767DD2677C3CA0098BAA8 /* PlayerState.swift in Sources */,
|
37B767DD2677C3CA0098BAA8 /* PlayerState.swift in Sources */,
|
||||||
37C7A905267905AE00E721B4 /* AVKeyValueStatus+String.swift in Sources */,
|
37C7A905267905AE00E721B4 /* AVKeyValueStatus+String.swift in Sources */,
|
||||||
|
Loading…
Reference in New Issue
Block a user