Add Sponsor Block and settings

This commit is contained in:
Arkadiusz Fal
2021-10-23 18:49:45 +02:00
parent e64a520d5e
commit 8e0af22b94
21 changed files with 362 additions and 114 deletions

View File

@@ -6,6 +6,7 @@ import Logging
import UIKit
#endif
import Siesta
import SwiftUI
import SwiftyJSON
final class PlayerModel: ObservableObject {
@@ -22,8 +23,8 @@ final class PlayerModel: ObservableObject {
@Published var stream: Stream?
@Published var currentRate: Float?
@Published var availableStreams = [Stream]() { didSet { rebuildStreamsMenu() } }
@Published var streamSelection: Stream? { didSet { rebuildStreamsMenu() } }
@Published var availableStreams = [Stream]() { didSet { rebuildTVMenu() } }
@Published var streamSelection: Stream? { didSet { rebuildTVMenu() } }
@Published var queue = [PlayerQueueItem]()
@Published var currentItem: PlayerQueueItem!
@@ -33,6 +34,11 @@ final class PlayerModel: ObservableObject {
@Published var playerNavigationLinkActive = false
@Published var sponsorBlock = SponsorBlockAPI()
@Published var segmentRestorationTime: CMTime?
@Published var lastSkipped: Segment? { didSet { rebuildTVMenu() } }
@Published var restoredSegments = [Segment]()
var accounts: AccountsModel
var instances: InstancesModel
@@ -117,6 +123,9 @@ final class PlayerModel: ObservableObject {
of video: Video,
preservingTime: Bool = false
) {
resetSegments()
sponsorBlock.loadSegments(videoID: video.videoID)
if let url = stream.singleAssetURL {
logger.info("playing stream with one asset\(stream.kind == .hls ? " (HLS)" : ""): \(url)")
@@ -218,7 +227,7 @@ final class PlayerModel: ObservableObject {
}
try! compositionTrack.insertTimeRange(
CMTimeRange(start: .zero, duration: CMTime(seconds: video.length, preferredTimescale: 1000)),
CMTimeRange(start: .zero, duration: CMTime(seconds: video.length, preferredTimescale: 1_000_000)),
of: assetTrack,
at: .zero
)
@@ -327,19 +336,28 @@ final class PlayerModel: ObservableObject {
player.seek(
to: time,
toleranceBefore: .init(seconds: 1, preferredTimescale: 1000),
toleranceBefore: .init(seconds: 1, preferredTimescale: 1_000_000),
toleranceAfter: .zero,
completionHandler: completionHandler
)
}
private func addTimeObserver() {
let interval = CMTime(seconds: 0.5, preferredTimescale: 1000)
let interval = CMTime(seconds: 0.5, preferredTimescale: 1_000_000)
timeObserver = player.addPeriodicTimeObserver(forInterval: interval, queue: .main) { _ in
self.currentRate = self.player.rate
self.currentItem?.playbackTime = self.player.currentTime()
self.currentItem?.videoDuration = self.player.currentItem?.asset.duration.seconds
guard !self.currentItem.isNil else {
return
}
let time = self.player.currentTime()
self.currentItem!.playbackTime = time
self.currentItem!.videoDuration = self.player.currentItem?.asset.duration.seconds
self.handleSegments(at: time)
}
}
}

View File

@@ -0,0 +1,77 @@
import CoreMedia
import Defaults
import Foundation
extension PlayerModel {
func handleSegments(at time: CMTime) {
if let segment = lastSkipped {
if time > CMTime(seconds: segment.end + 10, preferredTimescale: 1_000_000) {
resetLastSegment()
}
}
guard let firstSegment = sponsorBlock.segments.first(where: { $0.timeInSegment(time) }) else {
return
}
// find last segment in case they are 2 sec or less after each other
// to avoid multiple skips in a row
var nextSegments = [firstSegment]
while let segment = sponsorBlock.segments.first(where: {
$0.timeInSegment(CMTime(seconds: nextSegments.last!.end + 2, preferredTimescale: 1_000_000))
}) {
nextSegments.append(segment)
}
if let segmentToSkip = nextSegments.last(where: { $0.endTime <= playerItemDuration ?? .zero }),
self.shouldSkip(segmentToSkip, at: time)
{
skip(segmentToSkip, at: time)
}
}
private func skip(_ segment: Segment, at time: CMTime) {
guard segment.endTime.seconds <= playerItemDuration?.seconds ?? .infinity else {
logger.error("item time is: \(time.seconds) and trying to skip to \(playerItemDuration?.seconds ?? .infinity)")
return
}
player.seek(to: segment.endTime)
lastSkipped = segment
segmentRestorationTime = time
logger.info("SponsorBlock skipping to: \(segment.endTime)")
}
private func shouldSkip(_ segment: Segment, at time: CMTime) -> Bool {
guard isPlaying,
!restoredSegments.contains(segment),
Defaults[.sponsorBlockCategories].contains(segment.category)
else {
return false
}
return time.seconds - segment.start < 2 && segment.end - time.seconds > 2
}
func restoreLastSkippedSegment() {
guard let segment = lastSkipped,
let time = segmentRestorationTime
else {
return
}
restoredSegments.append(segment)
player.seek(to: time)
resetLastSegment()
}
private func resetLastSegment() {
lastSkipped = nil
segmentRestorationTime = nil
}
func resetSegments() {
resetLastSegment()
restoredSegments = []
}
}

View File

@@ -57,47 +57,13 @@ extension PlayerModel {
completionHandler: @escaping ([Stream]) -> Void
) {
instancesWithLoadedStreams.append(instance)
rebuildStreamsMenu()
rebuildTVMenu()
if instances.all.count == instancesWithLoadedStreams.count {
completionHandler(streams.sorted { $0.kind < $1.kind })
}
}
#if os(tvOS)
var streamsMenu: UIMenu {
UIMenu(
title: "Streams",
image: UIImage(systemName: "antenna.radiowaves.left.and.right"),
children: streamsMenuActions
)
}
var streamsMenuActions: [UIAction] {
guard !availableStreams.isEmpty else {
return [ // swiftlint:disable:this implicit_return
UIAction(title: "Empty", attributes: .disabled) { _ in }
]
}
return availableStreamsSorted.map { stream in
let state = stream == streamSelection ? UIAction.State.on : .off
return UIAction(title: stream.description, state: state) { _ in
self.streamSelection = stream
self.upgradeToStream(stream)
}
}
}
#endif
func rebuildStreamsMenu() {
#if os(tvOS)
avPlayerViewController?.transportBarCustomMenuItems = [streamsMenu]
#endif
}
func streamsWithInstance(instance: Instance, streams: [Stream]) -> [Stream] {
streams.map { stream in
stream.instance = instance

View File

@@ -0,0 +1,55 @@
import Foundation
#if !os(macOS)
import UIKit
#endif
extension PlayerModel {
#if os(tvOS)
var streamsMenu: UIMenu {
UIMenu(
title: "Streams",
image: UIImage(systemName: "antenna.radiowaves.left.and.right"),
children: streamsMenuActions
)
}
var streamsMenuActions: [UIAction] {
guard !availableStreams.isEmpty else {
return [ // swiftlint:disable:this implicit_return
UIAction(title: "Empty", attributes: .disabled) { _ in }
]
}
return availableStreamsSorted.map { stream in
let state = stream == streamSelection ? UIAction.State.on : .off
return UIAction(title: stream.description, state: state) { _ in
self.streamSelection = stream
self.upgradeToStream(stream)
}
}
}
var restoreLastSkippedSegmentAction: UIAction? {
guard let segment = lastSkipped else {
return nil // swiftlint:disable:this implicit_return
}
return UIAction(
title: "Restore \(segment.category)",
image: UIImage(systemName: "arrow.uturn.left.circle")
) { _ in
self.restoreLastSkippedSegment()
}
}
#endif
func rebuildTVMenu() {
#if os(tvOS)
avPlayerViewController?.transportBarCustomMenuItems = [
restoreLastSkippedSegmentAction,
streamsMenu
].compactMap { $0 }
#endif
}
}