-
Notifications
You must be signed in to change notification settings - Fork 433
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
1acddc7
commit 821c048
Showing
6 changed files
with
213 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
112 changes: 112 additions & 0 deletions
112
BilibiliLive/Component/Player/Plugins/SponsorSkipPlugin.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
// | ||
// SponsorSkipPlugin.swift | ||
// BilibiliLive | ||
// | ||
// Created by yicheng on 2/11/2024. | ||
// | ||
|
||
import AVKit | ||
|
||
class SponsorSkipPlugin: NSObject, CommonPlayerPlugin { | ||
private var clipInfos: [SponsorBlockRequest.SkipSegment] = [] | ||
private let bvid: String | ||
private let duration: Double | ||
private var observers = [Any]() | ||
private weak var playerVC: AVPlayerViewController? | ||
|
||
private var set = false | ||
|
||
init(bvid: String, duration: Int) { | ||
self.bvid = bvid | ||
self.duration = Double(duration) | ||
} | ||
|
||
func loadClips() async { | ||
do { | ||
clipInfos = try await SponsorBlockRequest.getSkipSegments(bvid: bvid) | ||
clipInfos = clipInfos.filter { | ||
abs(duration - $0.videoDuration) < 4 | ||
} | ||
|
||
Logger.debug("[SponsorBlockRequest] get segs:" + clipInfos.map { "\($0.start)-\($0.end)" }.joined(separator: ",")) | ||
if !set, let player = await playerVC?.player { | ||
set = true | ||
sendClipToPlayer(player: player) | ||
} | ||
} catch { | ||
print(error) | ||
} | ||
} | ||
|
||
func sendClipToPlayer(player: AVPlayer) { | ||
for clip in clipInfos { | ||
let start: CMTime | ||
let end: CMTime | ||
|
||
let buttonText: String | ||
let autoSkip = Settings.enableSponsorBlock == .jump | ||
if autoSkip { | ||
start = CMTime(seconds: clip.start - 5, preferredTimescale: 1) | ||
end = CMTime(seconds: clip.start, preferredTimescale: 1) | ||
buttonText = "取消跳过广告" | ||
} else { | ||
start = CMTime(seconds: clip.start, preferredTimescale: 1) | ||
end = CMTime(seconds: clip.end - 1, preferredTimescale: 1) | ||
buttonText = "跳过广告" | ||
} | ||
|
||
let skipAction = { [weak player, weak self] in | ||
player?.seek(to: CMTime(seconds: Double(clip.end), preferredTimescale: 1), toleranceBefore: .zero, toleranceAfter: .zero) | ||
self?.playerVC?.contextualActions = [] | ||
} | ||
|
||
let startObserver = player.addBoundaryTimeObserver(forTimes: [NSValue(time: start)], queue: .main) { | ||
[weak self] in | ||
guard let self = self else { return } | ||
let action: UIAction | ||
let identifier = UIAction.Identifier(clip.UUID) | ||
if autoSkip { | ||
action = UIAction(title: buttonText, identifier: identifier) { [weak self] _ in | ||
self?.playerVC?.contextualActions = [] | ||
} | ||
} else { | ||
action = UIAction(title: buttonText, identifier: identifier) { _ in skipAction() } | ||
} | ||
playerVC?.contextualActions = [action] | ||
} | ||
observers.append(startObserver) | ||
|
||
let endObserver = player.addBoundaryTimeObserver(forTimes: [NSValue(time: end)], queue: .main) { | ||
[weak self] in | ||
guard let self = self else { return } | ||
if let action = playerVC?.contextualActions.first, | ||
action.identifier.rawValue == clip.UUID, autoSkip | ||
{ | ||
skipAction() | ||
} | ||
playerVC?.contextualActions = [] | ||
} | ||
observers.append(endObserver) | ||
} | ||
} | ||
|
||
func playerDidLoad(playerVC: AVPlayerViewController) { | ||
self.playerVC = playerVC | ||
Task { | ||
await loadClips() | ||
} | ||
} | ||
|
||
func playerWillStart(player: AVPlayer) { | ||
if !clipInfos.isEmpty { | ||
set = true | ||
sendClipToPlayer(player: player) | ||
} | ||
} | ||
|
||
func playerDidCleanUp(player: AVPlayer) { | ||
for observer in observers { | ||
player.removeTimeObserver(observer) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
// | ||
// SponsorBlockRequest.swift | ||
// BilibiliLive | ||
// | ||
// Created by yicheng on 2/11/2024. | ||
// | ||
|
||
import Alamofire | ||
import CryptoKit | ||
import Foundation | ||
|
||
enum SponsorBlockRequest { | ||
class SkipSegment: Codable { | ||
let segment: [Double] | ||
let category: String | ||
let UUID: String | ||
let actionType: String | ||
let videoDuration: Double | ||
|
||
var vaild: Bool { | ||
segment.count == 2 | ||
} | ||
|
||
var start: Double { | ||
segment[0] | ||
} | ||
|
||
var end: Double { | ||
segment[1] | ||
} | ||
} | ||
|
||
enum Category: String, Codable { | ||
case sponsor | ||
} | ||
|
||
static let sponsorBlockAPI = "https://bsbsb.top/api/skipSegments/" | ||
|
||
static func getSkipSegments(bvid: String) async throws -> [SkipSegment] { | ||
class Infos: Codable { | ||
let segments: [SkipSegment] | ||
let videoID: String | ||
} | ||
|
||
let sha256 = SHA256.hash(data: bvid.data(using: .utf8)!) | ||
.map({ String(format: "%02x", $0) }).prefix(2).joined() | ||
let parameters = ["category": Category.sponsor.rawValue] | ||
|
||
let request = AF.request(sponsorBlockAPI + sha256, parameters: parameters) | ||
.serializingDecodable([Infos].self) | ||
do { | ||
let response = try await request.value | ||
|
||
let segs = response.filter({ $0.videoID == bvid }) | ||
.map({ $0.segments }) | ||
.flatMap({ $0 }) | ||
.filter({ $0.vaild }) | ||
return segs | ||
} catch { | ||
throw error | ||
} | ||
} | ||
} |