Skip to content

Commit 821c048

Browse files
committed
feat:(#123): 添加bilibili空降助手支持
1 parent 1acddc7 commit 821c048

File tree

6 files changed

+213
-0
lines changed

6 files changed

+213
-0
lines changed

BilibiliLive.xcodeproj/project.pbxproj

+8
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
49078E47291BEA2400F556BD /* PocketSVG in Frameworks */ = {isa = PBXBuildFile; productRef = 49078E46291BEA2400F556BD /* PocketSVG */; };
2121
490EC3E7290CC8F8001E00B6 /* RankingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 490EC3E6290CC8F8001E00B6 /* RankingViewController.swift */; };
2222
490EC3E9290CE23E001E00B6 /* BLSettingLineCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 490EC3E8290CE23E001E00B6 /* BLSettingLineCollectionViewCell.swift */; };
23+
492138A02CD5CA6000891D56 /* SponsorBlockRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4921389F2CD5CA6000891D56 /* SponsorBlockRequest.swift */; };
24+
492138A22CD5CDBA00891D56 /* SponsorSkipPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 492138A12CD5CDBA00891D56 /* SponsorSkipPlugin.swift */; };
2325
492731EE29096677005F5B0A /* HotViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 492731ED29096677005F5B0A /* HotViewController.swift */; };
2426
492AD7092BFF1E6C007221C8 /* CommonPlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 492AD7082BFF1E6C007221C8 /* CommonPlayerViewController.swift */; };
2527
492AD70B2BFF23B1007221C8 /* DanmuViewPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 492AD70A2BFF23B1007221C8 /* DanmuViewPlugin.swift */; };
@@ -165,6 +167,8 @@
165167
490425F629AB54B200CDBC60 /* CategoryViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryViewController.swift; sourceTree = "<group>"; };
166168
490EC3E6290CC8F8001E00B6 /* RankingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RankingViewController.swift; sourceTree = "<group>"; };
167169
490EC3E8290CE23E001E00B6 /* BLSettingLineCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BLSettingLineCollectionViewCell.swift; sourceTree = "<group>"; };
170+
4921389F2CD5CA6000891D56 /* SponsorBlockRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SponsorBlockRequest.swift; sourceTree = "<group>"; };
171+
492138A12CD5CDBA00891D56 /* SponsorSkipPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SponsorSkipPlugin.swift; sourceTree = "<group>"; };
168172
492731ED29096677005F5B0A /* HotViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HotViewController.swift; sourceTree = "<group>"; };
169173
492AD7082BFF1E6C007221C8 /* CommonPlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommonPlayerViewController.swift; sourceTree = "<group>"; };
170174
492AD70A2BFF23B1007221C8 /* DanmuViewPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DanmuViewPlugin.swift; sourceTree = "<group>"; };
@@ -476,6 +480,7 @@
476480
496E5A542C01CDBB0062951B /* DebugPlugin.swift */,
477481
496E5A562C01CDCA0062951B /* SpeedChangerPlugin.swift */,
478482
49D6A7112C0200ED0084A5A7 /* CommonPlayerPlugin.swift */,
483+
492138A12CD5CDBA00891D56 /* SponsorSkipPlugin.swift */,
479484
);
480485
path = Plugins;
481486
sourceTree = "<group>";
@@ -624,6 +629,7 @@
624629
49D39F27263AD40000F14497 /* WebRequest.swift */,
625630
F9D382B326359EF90070508F /* ApiRequest.swift */,
626631
F927ED8F2610A5E900EAB8E3 /* CookieManager.swift */,
632+
4921389F2CD5CA6000891D56 /* SponsorBlockRequest.swift */,
627633
);
628634
path = Request;
629635
sourceTree = "<group>";
@@ -903,6 +909,7 @@
903909
2DBE4C4D2628818F00D20413 /* HistoryViewController.swift in Sources */,
904910
492AD70D2BFF33DF007221C8 /* VideoPlayerViewController.swift in Sources */,
905911
F9171D6629026AC5002868C7 /* TitleSupplementaryView.swift in Sources */,
912+
492138A02CD5CA6000891D56 /* SponsorBlockRequest.swift in Sources */,
906913
F9B9EAE7261AC6F80045C2C6 /* BLTabBarViewController.swift in Sources */,
907914
498CF2A12B63AABE0009793E /* metablock.c in Sources */,
908915
F99D28EA26195EC200F8E66A /* UIView+Layout.swift in Sources */,
@@ -923,6 +930,7 @@
923930
49D6A7122C0200ED0084A5A7 /* CommonPlayerPlugin.swift in Sources */,
924931
490425F729AB54B200CDBC60 /* CategoryViewController.swift in Sources */,
925932
4973502D29162A6D0045C26B /* StandardVideoCollectionViewController.swift in Sources */,
933+
492138A22CD5CDBA00891D56 /* SponsorSkipPlugin.swift in Sources */,
926934
F927ED752610395300EAB8E3 /* DanmakuAsyncLayer.swift in Sources */,
927935
498CF2942B63AABE0009793E /* entropy_encode.c in Sources */,
928936
498CF2A82B63AABE0009793E /* huffman.c in Sources */,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
//
2+
// SponsorSkipPlugin.swift
3+
// BilibiliLive
4+
//
5+
// Created by yicheng on 2/11/2024.
6+
//
7+
8+
import AVKit
9+
10+
class SponsorSkipPlugin: NSObject, CommonPlayerPlugin {
11+
private var clipInfos: [SponsorBlockRequest.SkipSegment] = []
12+
private let bvid: String
13+
private let duration: Double
14+
private var observers = [Any]()
15+
private weak var playerVC: AVPlayerViewController?
16+
17+
private var set = false
18+
19+
init(bvid: String, duration: Int) {
20+
self.bvid = bvid
21+
self.duration = Double(duration)
22+
}
23+
24+
func loadClips() async {
25+
do {
26+
clipInfos = try await SponsorBlockRequest.getSkipSegments(bvid: bvid)
27+
clipInfos = clipInfos.filter {
28+
abs(duration - $0.videoDuration) < 4
29+
}
30+
31+
Logger.debug("[SponsorBlockRequest] get segs:" + clipInfos.map { "\($0.start)-\($0.end)" }.joined(separator: ","))
32+
if !set, let player = await playerVC?.player {
33+
set = true
34+
sendClipToPlayer(player: player)
35+
}
36+
} catch {
37+
print(error)
38+
}
39+
}
40+
41+
func sendClipToPlayer(player: AVPlayer) {
42+
for clip in clipInfos {
43+
let start: CMTime
44+
let end: CMTime
45+
46+
let buttonText: String
47+
let autoSkip = Settings.enableSponsorBlock == .jump
48+
if autoSkip {
49+
start = CMTime(seconds: clip.start - 5, preferredTimescale: 1)
50+
end = CMTime(seconds: clip.start, preferredTimescale: 1)
51+
buttonText = "取消跳过广告"
52+
} else {
53+
start = CMTime(seconds: clip.start, preferredTimescale: 1)
54+
end = CMTime(seconds: clip.end - 1, preferredTimescale: 1)
55+
buttonText = "跳过广告"
56+
}
57+
58+
let skipAction = { [weak player, weak self] in
59+
player?.seek(to: CMTime(seconds: Double(clip.end), preferredTimescale: 1), toleranceBefore: .zero, toleranceAfter: .zero)
60+
self?.playerVC?.contextualActions = []
61+
}
62+
63+
let startObserver = player.addBoundaryTimeObserver(forTimes: [NSValue(time: start)], queue: .main) {
64+
[weak self] in
65+
guard let self = self else { return }
66+
let action: UIAction
67+
let identifier = UIAction.Identifier(clip.UUID)
68+
if autoSkip {
69+
action = UIAction(title: buttonText, identifier: identifier) { [weak self] _ in
70+
self?.playerVC?.contextualActions = []
71+
}
72+
} else {
73+
action = UIAction(title: buttonText, identifier: identifier) { _ in skipAction() }
74+
}
75+
playerVC?.contextualActions = [action]
76+
}
77+
observers.append(startObserver)
78+
79+
let endObserver = player.addBoundaryTimeObserver(forTimes: [NSValue(time: end)], queue: .main) {
80+
[weak self] in
81+
guard let self = self else { return }
82+
if let action = playerVC?.contextualActions.first,
83+
action.identifier.rawValue == clip.UUID, autoSkip
84+
{
85+
skipAction()
86+
}
87+
playerVC?.contextualActions = []
88+
}
89+
observers.append(endObserver)
90+
}
91+
}
92+
93+
func playerDidLoad(playerVC: AVPlayerViewController) {
94+
self.playerVC = playerVC
95+
Task {
96+
await loadClips()
97+
}
98+
}
99+
100+
func playerWillStart(player: AVPlayer) {
101+
if !clipInfos.isEmpty {
102+
set = true
103+
sendClipToPlayer(player: player)
104+
}
105+
}
106+
107+
func playerDidCleanUp(player: AVPlayer) {
108+
for observer in observers {
109+
player.removeTimeObserver(observer)
110+
}
111+
}
112+
}

BilibiliLive/Component/Settings.swift

+20
Original file line numberDiff line numberDiff line change
@@ -101,13 +101,33 @@ enum Settings {
101101

102102
@UserDefault("Settings.ui.sideMenuAutoSelectChange", defaultValue: false)
103103
static var sideMenuAutoSelectChange: Bool
104+
105+
@UserDefaultCodable("Settings.SponsorBlockType", defaultValue: SponsorBlockType.none)
106+
static var enableSponsorBlock: SponsorBlockType
104107
}
105108

106109
struct MediaQuality {
107110
var qn: Int
108111
var fnval: Int
109112
}
110113

114+
enum SponsorBlockType: String, Codable, CaseIterable {
115+
case none
116+
case jump
117+
case tip
118+
119+
var title: String {
120+
switch self {
121+
case .none:
122+
return ""
123+
case .jump:
124+
return "自动跳过"
125+
case .tip:
126+
return "手动跳过"
127+
}
128+
}
129+
}
130+
111131
enum DanmuArea: Codable, CaseIterable {
112132
case style_75
113133
case style_50

BilibiliLive/Component/Video/NewVideoPlayerViewModel.swift

+5
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,11 @@ class VideoPlayerViewModel {
163163
plugins.append(clip)
164164
}
165165

166+
if Settings.enableSponsorBlock != .none, let bvid = data.detail?.View.bvid, let duration = data.detail?.View.duration {
167+
let sponsor = SponsorSkipPlugin(bvid: bvid, duration: duration)
168+
plugins.append(sponsor)
169+
}
170+
166171
if Settings.danmuMask {
167172
if let mask = data.playerInfo?.dm_mask,
168173
let video = data.videoPlayURLInfo.dash.video.first,

BilibiliLive/Module/Personal/SettingsViewController.swift

+5
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,11 @@ class SettingsViewController: UIViewController {
101101
}
102102
cellModels.append(hotWithoutCookie)
103103

104+
let sponsorBlock = cellModelWithActions(title: "空降助手广告屏蔽", message: "", current: Settings.enableSponsorBlock.title, options: SponsorBlockType.allCases, optionString: SponsorBlockType.allCases.map({ $0.title })) {
105+
Settings.enableSponsorBlock = $0
106+
}
107+
cellModels.append(sponsorBlock)
108+
104109
let continuePlay = CellModel(title: "从上次退出的位置继续播放", desp: Settings.continuePlay ? "" : "") {
105110
[weak self] in
106111
Settings.continuePlay.toggle()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
//
2+
// SponsorBlockRequest.swift
3+
// BilibiliLive
4+
//
5+
// Created by yicheng on 2/11/2024.
6+
//
7+
8+
import Alamofire
9+
import CryptoKit
10+
import Foundation
11+
12+
enum SponsorBlockRequest {
13+
class SkipSegment: Codable {
14+
let segment: [Double]
15+
let category: String
16+
let UUID: String
17+
let actionType: String
18+
let videoDuration: Double
19+
20+
var vaild: Bool {
21+
segment.count == 2
22+
}
23+
24+
var start: Double {
25+
segment[0]
26+
}
27+
28+
var end: Double {
29+
segment[1]
30+
}
31+
}
32+
33+
enum Category: String, Codable {
34+
case sponsor
35+
}
36+
37+
static let sponsorBlockAPI = "https://bsbsb.top/api/skipSegments/"
38+
39+
static func getSkipSegments(bvid: String) async throws -> [SkipSegment] {
40+
class Infos: Codable {
41+
let segments: [SkipSegment]
42+
let videoID: String
43+
}
44+
45+
let sha256 = SHA256.hash(data: bvid.data(using: .utf8)!)
46+
.map({ String(format: "%02x", $0) }).prefix(2).joined()
47+
let parameters = ["category": Category.sponsor.rawValue]
48+
49+
let request = AF.request(sponsorBlockAPI + sha256, parameters: parameters)
50+
.serializingDecodable([Infos].self)
51+
do {
52+
let response = try await request.value
53+
54+
let segs = response.filter({ $0.videoID == bvid })
55+
.map({ $0.segments })
56+
.flatMap({ $0 })
57+
.filter({ $0.vaild })
58+
return segs
59+
} catch {
60+
throw error
61+
}
62+
}
63+
}

0 commit comments

Comments
 (0)