Skip to content

Commit

Permalink
feat:(#123): 添加bilibili空降助手支持
Browse files Browse the repository at this point in the history
  • Loading branch information
yichengchen committed Nov 2, 2024
1 parent 1acddc7 commit 821c048
Show file tree
Hide file tree
Showing 6 changed files with 213 additions and 0 deletions.
8 changes: 8 additions & 0 deletions BilibiliLive.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
49078E47291BEA2400F556BD /* PocketSVG in Frameworks */ = {isa = PBXBuildFile; productRef = 49078E46291BEA2400F556BD /* PocketSVG */; };
490EC3E7290CC8F8001E00B6 /* RankingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 490EC3E6290CC8F8001E00B6 /* RankingViewController.swift */; };
490EC3E9290CE23E001E00B6 /* BLSettingLineCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 490EC3E8290CE23E001E00B6 /* BLSettingLineCollectionViewCell.swift */; };
492138A02CD5CA6000891D56 /* SponsorBlockRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4921389F2CD5CA6000891D56 /* SponsorBlockRequest.swift */; };
492138A22CD5CDBA00891D56 /* SponsorSkipPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 492138A12CD5CDBA00891D56 /* SponsorSkipPlugin.swift */; };
492731EE29096677005F5B0A /* HotViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 492731ED29096677005F5B0A /* HotViewController.swift */; };
492AD7092BFF1E6C007221C8 /* CommonPlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 492AD7082BFF1E6C007221C8 /* CommonPlayerViewController.swift */; };
492AD70B2BFF23B1007221C8 /* DanmuViewPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 492AD70A2BFF23B1007221C8 /* DanmuViewPlugin.swift */; };
Expand Down Expand Up @@ -165,6 +167,8 @@
490425F629AB54B200CDBC60 /* CategoryViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryViewController.swift; sourceTree = "<group>"; };
490EC3E6290CC8F8001E00B6 /* RankingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RankingViewController.swift; sourceTree = "<group>"; };
490EC3E8290CE23E001E00B6 /* BLSettingLineCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BLSettingLineCollectionViewCell.swift; sourceTree = "<group>"; };
4921389F2CD5CA6000891D56 /* SponsorBlockRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SponsorBlockRequest.swift; sourceTree = "<group>"; };
492138A12CD5CDBA00891D56 /* SponsorSkipPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SponsorSkipPlugin.swift; sourceTree = "<group>"; };
492731ED29096677005F5B0A /* HotViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HotViewController.swift; sourceTree = "<group>"; };
492AD7082BFF1E6C007221C8 /* CommonPlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommonPlayerViewController.swift; sourceTree = "<group>"; };
492AD70A2BFF23B1007221C8 /* DanmuViewPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DanmuViewPlugin.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -476,6 +480,7 @@
496E5A542C01CDBB0062951B /* DebugPlugin.swift */,
496E5A562C01CDCA0062951B /* SpeedChangerPlugin.swift */,
49D6A7112C0200ED0084A5A7 /* CommonPlayerPlugin.swift */,
492138A12CD5CDBA00891D56 /* SponsorSkipPlugin.swift */,
);
path = Plugins;
sourceTree = "<group>";
Expand Down Expand Up @@ -624,6 +629,7 @@
49D39F27263AD40000F14497 /* WebRequest.swift */,
F9D382B326359EF90070508F /* ApiRequest.swift */,
F927ED8F2610A5E900EAB8E3 /* CookieManager.swift */,
4921389F2CD5CA6000891D56 /* SponsorBlockRequest.swift */,
);
path = Request;
sourceTree = "<group>";
Expand Down Expand Up @@ -903,6 +909,7 @@
2DBE4C4D2628818F00D20413 /* HistoryViewController.swift in Sources */,
492AD70D2BFF33DF007221C8 /* VideoPlayerViewController.swift in Sources */,
F9171D6629026AC5002868C7 /* TitleSupplementaryView.swift in Sources */,
492138A02CD5CA6000891D56 /* SponsorBlockRequest.swift in Sources */,
F9B9EAE7261AC6F80045C2C6 /* BLTabBarViewController.swift in Sources */,
498CF2A12B63AABE0009793E /* metablock.c in Sources */,
F99D28EA26195EC200F8E66A /* UIView+Layout.swift in Sources */,
Expand All @@ -923,6 +930,7 @@
49D6A7122C0200ED0084A5A7 /* CommonPlayerPlugin.swift in Sources */,
490425F729AB54B200CDBC60 /* CategoryViewController.swift in Sources */,
4973502D29162A6D0045C26B /* StandardVideoCollectionViewController.swift in Sources */,
492138A22CD5CDBA00891D56 /* SponsorSkipPlugin.swift in Sources */,
F927ED752610395300EAB8E3 /* DanmakuAsyncLayer.swift in Sources */,
498CF2942B63AABE0009793E /* entropy_encode.c in Sources */,
498CF2A82B63AABE0009793E /* huffman.c in Sources */,
Expand Down
112 changes: 112 additions & 0 deletions BilibiliLive/Component/Player/Plugins/SponsorSkipPlugin.swift
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)
}
}
}
20 changes: 20 additions & 0 deletions BilibiliLive/Component/Settings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -101,13 +101,33 @@ enum Settings {

@UserDefault("Settings.ui.sideMenuAutoSelectChange", defaultValue: false)
static var sideMenuAutoSelectChange: Bool

@UserDefaultCodable("Settings.SponsorBlockType", defaultValue: SponsorBlockType.none)
static var enableSponsorBlock: SponsorBlockType
}

struct MediaQuality {
var qn: Int
var fnval: Int
}

enum SponsorBlockType: String, Codable, CaseIterable {
case none
case jump
case tip

var title: String {
switch self {
case .none:
return ""
case .jump:
return "自动跳过"
case .tip:
return "手动跳过"
}
}
}

enum DanmuArea: Codable, CaseIterable {
case style_75
case style_50
Expand Down
5 changes: 5 additions & 0 deletions BilibiliLive/Component/Video/NewVideoPlayerViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,11 @@ class VideoPlayerViewModel {
plugins.append(clip)
}

if Settings.enableSponsorBlock != .none, let bvid = data.detail?.View.bvid, let duration = data.detail?.View.duration {
let sponsor = SponsorSkipPlugin(bvid: bvid, duration: duration)
plugins.append(sponsor)
}

if Settings.danmuMask {
if let mask = data.playerInfo?.dm_mask,
let video = data.videoPlayURLInfo.dash.video.first,
Expand Down
5 changes: 5 additions & 0 deletions BilibiliLive/Module/Personal/SettingsViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,11 @@ class SettingsViewController: UIViewController {
}
cellModels.append(hotWithoutCookie)

let sponsorBlock = cellModelWithActions(title: "空降助手广告屏蔽", message: "", current: Settings.enableSponsorBlock.title, options: SponsorBlockType.allCases, optionString: SponsorBlockType.allCases.map({ $0.title })) {
Settings.enableSponsorBlock = $0
}
cellModels.append(sponsorBlock)

let continuePlay = CellModel(title: "从上次退出的位置继续播放", desp: Settings.continuePlay ? "" : "") {
[weak self] in
Settings.continuePlay.toggle()
Expand Down
63 changes: 63 additions & 0 deletions BilibiliLive/Request/SponsorBlockRequest.swift
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
}
}
}

0 comments on commit 821c048

Please # to comment.