Compare commits

...

7 Commits

Author SHA1 Message Date
yicheng 5c782e09f5 Merge remote-tracking branch 'origin/main' into feat/player-refactor
# Conflicts:
#	BilibiliLive.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
2024-06-10 16:52:54 +08:00
yicheng 003a81d703 misc: rename NewCommonPlayerViewController.swift 2024-06-10 16:52:24 +08:00
dependabot[bot] 31de56716d
build(deps): bump rexml from 3.2.5 to 3.2.9 (#108)
Bumps [rexml](https://github.com/ruby/rexml) from 3.2.5 to 3.2.9.
- [Release notes](https://github.com/ruby/rexml/releases)
- [Changelog](https://github.com/ruby/rexml/blob/master/NEWS.md)
- [Commits](https://github.com/ruby/rexml/compare/v3.2.5...v3.2.9)

---
updated-dependencies:
- dependency-name: rexml
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-10 16:45:08 +08:00
yicheng 68c3aff948 添加港澳台解锁支持 2024-06-10 16:44:49 +08:00
yicheng 93a4c03ca7 remove old commonPlayer 2024-06-10 16:20:25 +08:00
yicheng 118bb3dff3 misc: update github action macos version 2024-06-10 15:28:48 +08:00
Yam Liu 09d30ee16c
feat(Comment): support multi-level comment preview, slightly refactor ReplyCell for reuse. (#107)
Co-authored-by: Yam <yamliu@duck.com>
2024-06-10 15:25:31 +08:00
34 changed files with 840 additions and 1075 deletions

View File

@ -7,7 +7,7 @@ on:
jobs:
build:
name: Build ipa
runs-on: macos-13
runs-on: macos-14
steps:
- name: Checkout
@ -23,21 +23,21 @@ jobs:
bundle install -j 4
bundle exec fastlane build_unsign_ipa
- name: List, filter and delete artifacts
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
uses: actions/github-script@v6
id: artifact
with:
script: |
const { owner, repo } = context.issue
# - name: List, filter and delete artifacts
# if: github.ref == 'refs/heads/main' && github.event_name == 'push'
# uses: actions/github-script@v6
# id: artifact
# with:
# script: |
# const { owner, repo } = context.issue
const res = await github.rest.actions.listArtifactsForRepo({
owner,
repo,
})
# const res = await github.rest.actions.listArtifactsForRepo({
# owner,
# repo,
# })
res.data.artifacts
.forEach(({ id }) => { github.rest.actions.deleteArtifact({ owner, repo, artifact_id: id, }) })
# res.data.artifacts
# .forEach(({ id }) => { github.rest.actions.deleteArtifact({ owner, repo, artifact_id: id, }) })
- name: Upload latest artifact
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
@ -45,4 +45,4 @@ jobs:
with:
name: main-unsigned.ipa
path: BilbiliAtvDemo.ipa
retention-days: 30
retention-days: 60

View File

@ -11,15 +11,19 @@
0A41EE1C2A63102B0066444C /* dm.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A41EE1A2A63102B0066444C /* dm.pb.swift */; };
0A41EE1D2A63102B0066444C /* dmView.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A41EE1B2A63102B0066444C /* dmView.pb.swift */; };
27FECFCC2B0B98F400EC6A6D /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 27FECFCB2B0B98F400EC6A6D /* Localizable.xcstrings */; };
2806E51E2C1593A000164C10 /* LookinServer in Frameworks */ = {isa = PBXBuildFile; productRef = 2806E51D2C1593A000164C10 /* LookinServer */; };
2806E5202C15A59E00164C10 /* ReplyDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2806E51F2C15A59E00164C10 /* ReplyDetailViewController.swift */; };
2806E5232C16011B00164C10 /* ReplyCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2806E5212C16011B00164C10 /* ReplyCell.swift */; };
2806E5242C16011B00164C10 /* ReplyCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 2806E5222C16011B00164C10 /* ReplyCell.xib */; };
2DBE4C4D2628818F00D20413 /* HistoryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DBE4C4C2628818F00D20413 /* HistoryViewController.swift */; };
490425F729AB54B200CDBC60 /* CategoryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 490425F629AB54B200CDBC60 /* CategoryViewController.swift */; };
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 */; };
492731EE29096677005F5B0A /* HotViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 492731ED29096677005F5B0A /* HotViewController.swift */; };
492AD7092BFF1E6C007221C8 /* NewCommonPlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 492AD7082BFF1E6C007221C8 /* NewCommonPlayerViewController.swift */; };
492AD7092BFF1E6C007221C8 /* CommonPlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 492AD7082BFF1E6C007221C8 /* CommonPlayerViewController.swift */; };
492AD70B2BFF23B1007221C8 /* DanmuViewPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 492AD70A2BFF23B1007221C8 /* DanmuViewPlugin.swift */; };
492AD70D2BFF33DF007221C8 /* NewVideoPlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 492AD70C2BFF33DF007221C8 /* NewVideoPlayerViewController.swift */; };
492AD70D2BFF33DF007221C8 /* VideoPlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 492AD70C2BFF33DF007221C8 /* VideoPlayerViewController.swift */; };
492AD70F2BFF6761007221C8 /* NewVideoPlayerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 492AD70E2BFF6761007221C8 /* NewVideoPlayerViewModel.swift */; };
492AD7112C001C7B007221C8 /* BVideoPlayPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 492AD7102C001C7B007221C8 /* BVideoPlayPlugin.swift */; };
492AD7132C001CA7007221C8 /* String+Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = 492AD7122C001CA7007221C8 /* String+Error.swift */; };
@ -38,15 +42,18 @@
49508E0F2943420100D26812 /* CocoaLumberjack in Frameworks */ = {isa = PBXBuildFile; productRef = 49508E0E2943420100D26812 /* CocoaLumberjack */; };
49508E112943420100D26812 /* CocoaLumberjackSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 49508E102943420100D26812 /* CocoaLumberjackSwift */; };
496400D32943431E0098ACA6 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 496400D22943431E0098ACA6 /* Logger.swift */; };
496E5A4D2C018F150062951B /* BVideoClipsPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 496E5A4C2C018F150062951B /* BVideoClipsPlugin.swift */; };
496E5A4F2C0194720062951B /* MaskViewPugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 496E5A4E2C0194720062951B /* MaskViewPugin.swift */; };
496E5A512C0194CD0062951B /* BUpnpPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 496E5A502C0194CD0062951B /* BUpnpPlugin.swift */; };
496E5A532C01B1CA0062951B /* BVideoInfoPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 496E5A522C01B1CA0062951B /* BVideoInfoPlugin.swift */; };
496E5A552C01CDBB0062951B /* DebugPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 496E5A542C01CDBB0062951B /* DebugPlugin.swift */; };
496E5A572C01CDCA0062951B /* SpeedChangerPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 496E5A562C01CDCA0062951B /* SpeedChangerPlugin.swift */; };
4973502B29161B770045C26B /* WeeklyWatchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4973502A29161B770045C26B /* WeeklyWatchViewController.swift */; };
4973502D29162A6D0045C26B /* StandardVideoCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4973502C29162A6D0045C26B /* StandardVideoCollectionViewController.swift */; };
497361082BF1A16600ED213F /* Keys.swift in Sources */ = {isa = PBXBuildFile; fileRef = 497361072BF1A16600ED213F /* Keys.swift */; };
497CF22F2C16EBA4006E1488 /* Published+..swift in Sources */ = {isa = PBXBuildFile; fileRef = 497CF22E2C16EBA4006E1488 /* Published+..swift */; };
497CF2312C16ED45006E1488 /* MaskProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 497CF2302C16ED45006E1488 /* MaskProvider.swift */; };
497CF2372C16EDE5006E1488 /* BVideoClipsPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 497CF2342C16EDE5006E1488 /* BVideoClipsPlugin.swift */; };
497CF2382C16EDE5006E1488 /* BVideoInfoPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 497CF2352C16EDE5006E1488 /* BVideoInfoPlugin.swift */; };
497CF2392C16EDE5006E1488 /* VideoPlayListPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 497CF2362C16EDE5006E1488 /* VideoPlayListPlugin.swift */; };
498CF2902B63AABE0009793E /* dictionary_hash.c in Sources */ = {isa = PBXBuildFile; fileRef = 498CF2422B63AABE0009793E /* dictionary_hash.c */; };
498CF2912B63AABE0009793E /* backward_references_hq.c in Sources */ = {isa = PBXBuildFile; fileRef = 498CF2432B63AABE0009793E /* backward_references_hq.c */; };
498CF2922B63AABE0009793E /* histogram.c in Sources */ = {isa = PBXBuildFile; fileRef = 498CF2462B63AABE0009793E /* histogram.c */; };
@ -85,7 +92,6 @@
49D250A22C11A82B00173908 /* AVPlayerMetaUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49D250A12C11A82B00173908 /* AVPlayerMetaUtils.swift */; };
49D39F28263AD40000F14497 /* WebRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49D39F27263AD40000F14497 /* WebRequest.swift */; };
49D6A7122C0200ED0084A5A7 /* CommonPlayerPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49D6A7112C0200ED0084A5A7 /* CommonPlayerPlugin.swift */; };
49D6A7142C0363F10084A5A7 /* VideoPlayListPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49D6A7132C0363F10084A5A7 /* VideoPlayListPlugin.swift */; };
49DA01A0296C466C00EEAE15 /* AVInfoPanelCollectionViewThumbnailCell+Hook.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49DA019F296C466C00EEAE15 /* AVInfoPanelCollectionViewThumbnailCell+Hook.swift */; };
49E5F85028AF73C500FAA3CE /* BilibiliVideoResourceLoaderDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49E5F84F28AF73C500FAA3CE /* BilibiliVideoResourceLoaderDelegate.swift */; };
49F9186E2931E3C9001D3EC3 /* DLNAInfo.xml in Resources */ = {isa = PBXBuildFile; fileRef = 49F9186D2931E3C9001D3EC3 /* DLNAInfo.xml */; };
@ -119,8 +125,6 @@
F927ED902610A5E900EAB8E3 /* CookieManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F927ED8F2610A5E900EAB8E3 /* CookieManager.swift */; };
F927ED992610AD8D00EAB8E3 /* LiveViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F927ED982610AD8D00EAB8E3 /* LiveViewController.swift */; };
F927ED9F2610B5C300EAB8E3 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = F927ED9E2610B5C300EAB8E3 /* Kingfisher */; };
F9562C92261A0D2200573B74 /* VideoPlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9562C91261A0D2200573B74 /* VideoPlayerViewController.swift */; };
F99D28E12619591300F8E66A /* CommonPlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F99D28E02619591300F8E66A /* CommonPlayerViewController.swift */; };
F99D28EA26195EC200F8E66A /* UIView+Layout.swift in Sources */ = {isa = PBXBuildFile; fileRef = F99D28E926195EC200F8E66A /* UIView+Layout.swift */; };
F99D28F72619F5F000F8E66A /* FollowsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F99D28F62619F5F000F8E66A /* FollowsViewController.swift */; };
F9B57354260F5F7400771ED5 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9B57353260F5F7400771ED5 /* AppDelegate.swift */; };
@ -154,14 +158,17 @@
0A41EE1A2A63102B0066444C /* dm.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = dm.pb.swift; sourceTree = "<group>"; };
0A41EE1B2A63102B0066444C /* dmView.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = dmView.pb.swift; sourceTree = "<group>"; };
27FECFCB2B0B98F400EC6A6D /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
2806E51F2C15A59E00164C10 /* ReplyDetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyDetailViewController.swift; sourceTree = "<group>"; };
2806E5212C16011B00164C10 /* ReplyCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ReplyCell.swift; path = BilibiliLive/Component/View/ReplyCell.swift; sourceTree = SOURCE_ROOT; };
2806E5222C16011B00164C10 /* ReplyCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = ReplyCell.xib; path = BilibiliLive/Component/View/ReplyCell.xib; sourceTree = SOURCE_ROOT; };
2DBE4C4C2628818F00D20413 /* HistoryViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryViewController.swift; sourceTree = "<group>"; };
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>"; };
492731ED29096677005F5B0A /* HotViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HotViewController.swift; sourceTree = "<group>"; };
492AD7082BFF1E6C007221C8 /* NewCommonPlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewCommonPlayerViewController.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>"; };
492AD70C2BFF33DF007221C8 /* NewVideoPlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewVideoPlayerViewController.swift; sourceTree = "<group>"; };
492AD70C2BFF33DF007221C8 /* VideoPlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerViewController.swift; sourceTree = "<group>"; };
492AD70E2BFF6761007221C8 /* NewVideoPlayerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewVideoPlayerViewModel.swift; sourceTree = "<group>"; };
492AD7102C001C7B007221C8 /* BVideoPlayPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BVideoPlayPlugin.swift; sourceTree = "<group>"; };
492AD7122C001CA7007221C8 /* String+Error.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Error.swift"; sourceTree = "<group>"; };
@ -179,15 +186,18 @@
49474212290509F6005D6885 /* DateFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateFormatter.swift; sourceTree = "<group>"; };
4947423A2906B308005D6885 /* BLTextOnlyCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BLTextOnlyCollectionViewCell.swift; sourceTree = "<group>"; };
496400D22943431E0098ACA6 /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = "<group>"; };
496E5A4C2C018F150062951B /* BVideoClipsPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BVideoClipsPlugin.swift; sourceTree = "<group>"; };
496E5A4E2C0194720062951B /* MaskViewPugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaskViewPugin.swift; sourceTree = "<group>"; };
496E5A502C0194CD0062951B /* BUpnpPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BUpnpPlugin.swift; sourceTree = "<group>"; };
496E5A522C01B1CA0062951B /* BVideoInfoPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BVideoInfoPlugin.swift; sourceTree = "<group>"; };
496E5A542C01CDBB0062951B /* DebugPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugPlugin.swift; sourceTree = "<group>"; };
496E5A562C01CDCA0062951B /* SpeedChangerPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpeedChangerPlugin.swift; sourceTree = "<group>"; };
4973502A29161B770045C26B /* WeeklyWatchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeeklyWatchViewController.swift; sourceTree = "<group>"; };
4973502C29162A6D0045C26B /* StandardVideoCollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StandardVideoCollectionViewController.swift; sourceTree = "<group>"; };
497361072BF1A16600ED213F /* Keys.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Keys.swift; sourceTree = "<group>"; };
497CF22E2C16EBA4006E1488 /* Published+..swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Published+..swift"; sourceTree = "<group>"; };
497CF2302C16ED45006E1488 /* MaskProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaskProvider.swift; sourceTree = "<group>"; };
497CF2342C16EDE5006E1488 /* BVideoClipsPlugin.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BVideoClipsPlugin.swift; sourceTree = "<group>"; };
497CF2352C16EDE5006E1488 /* BVideoInfoPlugin.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BVideoInfoPlugin.swift; sourceTree = "<group>"; };
497CF2362C16EDE5006E1488 /* VideoPlayListPlugin.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoPlayListPlugin.swift; sourceTree = "<group>"; };
498CF2352B63AABE0009793E /* NSData+BrotliCompression.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSData+BrotliCompression.h"; sourceTree = "<group>"; };
498CF2362B63AABE0009793E /* LMBrotliCompression.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LMBrotliCompression.h; sourceTree = "<group>"; };
498CF2372B63AABE0009793E /* BrotliKit.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BrotliKit.h; sourceTree = "<group>"; };
@ -281,7 +291,6 @@
49D250A12C11A82B00173908 /* AVPlayerMetaUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVPlayerMetaUtils.swift; sourceTree = "<group>"; };
49D39F27263AD40000F14497 /* WebRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebRequest.swift; sourceTree = "<group>"; };
49D6A7112C0200ED0084A5A7 /* CommonPlayerPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommonPlayerPlugin.swift; sourceTree = "<group>"; };
49D6A7132C0363F10084A5A7 /* VideoPlayListPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayListPlugin.swift; sourceTree = "<group>"; };
49DA019F296C466C00EEAE15 /* AVInfoPanelCollectionViewThumbnailCell+Hook.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AVInfoPanelCollectionViewThumbnailCell+Hook.swift"; sourceTree = "<group>"; };
49E5F84F28AF73C500FAA3CE /* BilibiliVideoResourceLoaderDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BilibiliVideoResourceLoaderDelegate.swift; sourceTree = "<group>"; };
49F9186D2931E3C9001D3EC3 /* DLNAInfo.xml */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = DLNAInfo.xml; sourceTree = "<group>"; };
@ -312,9 +321,7 @@
F927ED8C2610A5A800EAB8E3 /* LoginViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginViewController.swift; sourceTree = "<group>"; };
F927ED8F2610A5E900EAB8E3 /* CookieManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CookieManager.swift; sourceTree = "<group>"; };
F927ED982610AD8D00EAB8E3 /* LiveViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveViewController.swift; sourceTree = "<group>"; };
F9562C91261A0D2200573B74 /* VideoPlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerViewController.swift; sourceTree = "<group>"; };
F99D28DA2618A55900F8E66A /* BilibiliLive-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "BilibiliLive-Bridging-Header.h"; sourceTree = "<group>"; };
F99D28E02619591300F8E66A /* CommonPlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommonPlayerViewController.swift; sourceTree = "<group>"; };
F99D28E926195EC200F8E66A /* UIView+Layout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Layout.swift"; sourceTree = "<group>"; };
F99D28F62619F5F000F8E66A /* FollowsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowsViewController.swift; sourceTree = "<group>"; };
F9B57350260F5F7400771ED5 /* BilibiliLive.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = BilibiliLive.app; sourceTree = BUILT_PRODUCTS_DIR; };
@ -349,6 +356,7 @@
49508E0F2943420100D26812 /* CocoaLumberjack in Frameworks */,
499C75EC293058C9003160FB /* CocoaAsyncSocket in Frameworks */,
F927ED9F2610B5C300EAB8E3 /* Kingfisher in Frameworks */,
2806E51E2C1593A000164C10 /* LookinServer in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -387,7 +395,6 @@
F927ED982610AD8D00EAB8E3 /* LiveViewController.swift */,
F9B57355260F5F7400771ED5 /* LivePlayerViewController.swift */,
F927ED672610113A00EAB8E3 /* LiveDanMuProvider.swift */,
49D2509F2C118FA700173908 /* URLPlayPlugin.swift */,
);
path = Live;
sourceTree = "<group>";
@ -395,18 +402,15 @@
49389D5D28AFE6DC00B9DAFD /* Video */ = {
isa = PBXGroup;
children = (
F9562C91261A0D2200573B74 /* VideoPlayerViewController.swift */,
F9EDADD2262AA421007CB99F /* VideoDetailViewController.swift */,
2806E51F2C15A59E00164C10 /* ReplyDetailViewController.swift */,
2806E5212C16011B00164C10 /* ReplyCell.swift */,
2806E5222C16011B00164C10 /* ReplyCell.xib */,
49389D6128AFEA2900B9DAFD /* VideoDanmuProvider.swift */,
498DB1DE291BC24700F95607 /* BMaskProvider.swift */,
49FB8EBF291F4C520045D5DE /* VMaskProvider.swift */,
492AD70C2BFF33DF007221C8 /* NewVideoPlayerViewController.swift */,
497CF2322C16EDAC006E1488 /* MaskProvider */,
492AD70C2BFF33DF007221C8 /* VideoPlayerViewController.swift */,
492AD70E2BFF6761007221C8 /* NewVideoPlayerViewModel.swift */,
492AD7102C001C7B007221C8 /* BVideoPlayPlugin.swift */,
496E5A4C2C018F150062951B /* BVideoClipsPlugin.swift */,
496E5A502C0194CD0062951B /* BUpnpPlugin.swift */,
496E5A522C01B1CA0062951B /* BVideoInfoPlugin.swift */,
49D6A7132C0363F10084A5A7 /* VideoPlayListPlugin.swift */,
497CF2332C16EDC0006E1488 /* Plugins */,
);
path = Video;
sourceTree = "<group>";
@ -425,6 +429,7 @@
AE2B41562914C02000BF2B0B /* Int.swift */,
49DA019F296C466C00EEAE15 /* AVInfoPanelCollectionViewThumbnailCell+Hook.swift */,
492AD7122C001CA7007221C8 /* String+Error.swift */,
497CF22E2C16EBA4006E1488 /* Published+..swift */,
);
path = Extensions;
sourceTree = "<group>";
@ -440,6 +445,41 @@
path = View;
sourceTree = "<group>";
};
497CF2322C16EDAC006E1488 /* MaskProvider */ = {
isa = PBXGroup;
children = (
498DB1DE291BC24700F95607 /* BMaskProvider.swift */,
49FB8EBF291F4C520045D5DE /* VMaskProvider.swift */,
497CF2302C16ED45006E1488 /* MaskProvider.swift */,
);
path = MaskProvider;
sourceTree = "<group>";
};
497CF2332C16EDC0006E1488 /* Plugins */ = {
isa = PBXGroup;
children = (
497CF2342C16EDE5006E1488 /* BVideoClipsPlugin.swift */,
497CF2352C16EDE5006E1488 /* BVideoInfoPlugin.swift */,
497CF2362C16EDE5006E1488 /* VideoPlayListPlugin.swift */,
492AD7102C001C7B007221C8 /* BVideoPlayPlugin.swift */,
496E5A502C0194CD0062951B /* BUpnpPlugin.swift */,
);
path = Plugins;
sourceTree = "<group>";
};
497CF23A2C16EE04006E1488 /* Plugins */ = {
isa = PBXGroup;
children = (
49D2509F2C118FA700173908 /* URLPlayPlugin.swift */,
492AD70A2BFF23B1007221C8 /* DanmuViewPlugin.swift */,
496E5A4E2C0194720062951B /* MaskViewPugin.swift */,
496E5A542C01CDBB0062951B /* DebugPlugin.swift */,
496E5A562C01CDCA0062951B /* SpeedChangerPlugin.swift */,
49D6A7112C0200ED0084A5A7 /* CommonPlayerPlugin.swift */,
);
path = Plugins;
sourceTree = "<group>";
};
498CF2342B63AABE0009793E /* BrotliKit */ = {
isa = PBXGroup;
children = (
@ -665,14 +705,9 @@
isa = PBXGroup;
children = (
49E5F84F28AF73C500FAA3CE /* BilibiliVideoResourceLoaderDelegate.swift */,
F99D28E02619591300F8E66A /* CommonPlayerViewController.swift */,
49FB8EE829208EBE0045D5DE /* SidxParseUtil.swift */,
492AD7082BFF1E6C007221C8 /* NewCommonPlayerViewController.swift */,
492AD70A2BFF23B1007221C8 /* DanmuViewPlugin.swift */,
496E5A4E2C0194720062951B /* MaskViewPugin.swift */,
496E5A542C01CDBB0062951B /* DebugPlugin.swift */,
496E5A562C01CDCA0062951B /* SpeedChangerPlugin.swift */,
49D6A7112C0200ED0084A5A7 /* CommonPlayerPlugin.swift */,
492AD7082BFF1E6C007221C8 /* CommonPlayerViewController.swift */,
497CF23A2C16EE04006E1488 /* Plugins */,
49D250A12C11A82B00173908 /* AVPlayerMetaUtils.swift */,
);
path = Player;
@ -745,6 +780,7 @@
49508E0E2943420100D26812 /* CocoaLumberjack */,
49508E102943420100D26812 /* CocoaLumberjackSwift */,
0A41EE182A630FEA0066444C /* SwiftProtobuf */,
2806E51D2C1593A000164C10 /* LookinServer */,
);
productName = BilibiliLive;
productReference = F9B57350260F5F7400771ED5 /* BilibiliLive.app */;
@ -788,6 +824,7 @@
499C76142931A7AE003160FB /* XCRemoteSwiftPackageReference "SwiftyXMLParser" */,
49508E0D2943420100D26812 /* XCRemoteSwiftPackageReference "CocoaLumberjack" */,
0A41EE172A630FEA0066444C /* XCRemoteSwiftPackageReference "swift-protobuf" */,
2806E51C2C1593A000164C10 /* XCRemoteSwiftPackageReference "LookinServer" */,
);
productRefGroup = F9B57351260F5F7400771ED5 /* Products */;
projectDirPath = "";
@ -807,6 +844,7 @@
49F918722931E927001D3EC3 /* AvTransportScpd.xml in Resources */,
F9B5735E260F5F7600771ED5 /* LaunchScreen.storyboard in Resources */,
F9B5735B260F5F7600771ED5 /* Assets.xcassets in Resources */,
2806E5242C16011B00164C10 /* ReplyCell.xib in Resources */,
49F9186E2931E3C9001D3EC3 /* DLNAInfo.xml in Resources */,
27FECFCC2B0B98F400EC6A6D /* Localizable.xcstrings in Resources */,
F9B57359260F5F7400771ED5 /* Main.storyboard in Resources */,
@ -844,17 +882,18 @@
files = (
F9B9EAED261B25E40045C2C6 /* ToViewViewController.swift in Sources */,
4973502B29161B770045C26B /* WeeklyWatchViewController.swift in Sources */,
496E5A4D2C018F150062951B /* BVideoClipsPlugin.swift in Sources */,
AE7A3B20290298BE006FEBB0 /* Colors.swift in Sources */,
496E5A4F2C0194720062951B /* MaskViewPugin.swift in Sources */,
49FB8EC0291F4C520045D5DE /* VMaskProvider.swift in Sources */,
497361082BF1A16600ED213F /* Keys.swift in Sources */,
49E5F85028AF73C500FAA3CE /* BilibiliVideoResourceLoaderDelegate.swift in Sources */,
498CF2A22B63AABE0009793E /* dictionary.c in Sources */,
2806E5202C15A59E00164C10 /* ReplyDetailViewController.swift in Sources */,
494741C82902C45D005D6885 /* Array+..swift in Sources */,
498CF29E2B63AABE0009793E /* bit_cost.c in Sources */,
498CF29B2B63AABE0009793E /* utf8_util.c in Sources */,
498CF2AA2B63AABE0009793E /* LMBrotliCompressor.m in Sources */,
497CF22F2C16EBA4006E1488 /* Published+..swift in Sources */,
AEA6AB1628FFE951007CE72E /* SettingsViewController.swift in Sources */,
49389D8928B0A1B700B9DAFD /* UIViewController+Ext.swift in Sources */,
AE2B41572914C02000BF2B0B /* Int.swift in Sources */,
@ -862,10 +901,9 @@
498CF2A72B63AABE0009793E /* decode.c in Sources */,
F90AAE04265549B5008DE7C2 /* FeedViewController.swift in Sources */,
2DBE4C4D2628818F00D20413 /* HistoryViewController.swift in Sources */,
492AD70D2BFF33DF007221C8 /* NewVideoPlayerViewController.swift in Sources */,
492AD70D2BFF33DF007221C8 /* VideoPlayerViewController.swift in Sources */,
F9171D6629026AC5002868C7 /* TitleSupplementaryView.swift in Sources */,
F9B9EAE7261AC6F80045C2C6 /* BLTabBarViewController.swift in Sources */,
F9562C92261A0D2200573B74 /* VideoPlayerViewController.swift in Sources */,
498CF2A12B63AABE0009793E /* metablock.c in Sources */,
F99D28EA26195EC200F8E66A /* UIView+Layout.swift in Sources */,
498CF29D2B63AABE0009793E /* brotli_bit_stream.c in Sources */,
@ -874,6 +912,7 @@
49FB8EE929208EBE0045D5DE /* SidxParseUtil.swift in Sources */,
F9171D6129010429002868C7 /* UICollectionView+..swift in Sources */,
498CF2982B63AABE0009793E /* encoder_dict.c in Sources */,
497CF2312C16ED45006E1488 /* MaskProvider.swift in Sources */,
AE2B41552912706700BF2B0B /* SearchResultViewController.swift in Sources */,
499C75EE29305A1E003160FB /* BiliBiliUpnpDMR.swift in Sources */,
49D250A22C11A82B00173908 /* AVPlayerMetaUtils.swift in Sources */,
@ -897,7 +936,7 @@
492AD7112C001C7B007221C8 /* BVideoPlayPlugin.swift in Sources */,
F927ED742610395300EAB8E3 /* DanmakuView.swift in Sources */,
F927ED782610395300EAB8E3 /* DanmakuTrack.swift in Sources */,
492AD7092BFF1E6C007221C8 /* NewCommonPlayerViewController.swift in Sources */,
492AD7092BFF1E6C007221C8 /* CommonPlayerViewController.swift in Sources */,
498CF29C2B63AABE0009793E /* compress_fragment.c in Sources */,
499C760F2930E068003160FB /* NVASocket.swift in Sources */,
498CF2912B63AABE0009793E /* backward_references_hq.c in Sources */,
@ -908,19 +947,20 @@
F927ED992610AD8D00EAB8E3 /* LiveViewController.swift in Sources */,
492AD7132C001CA7007221C8 /* String+Error.swift in Sources */,
490EC3E7290CC8F8001E00B6 /* RankingViewController.swift in Sources */,
2806E5232C16011B00164C10 /* ReplyCell.swift in Sources */,
F927ED732610395300EAB8E3 /* DanmakuCell.swift in Sources */,
497CF2392C16EDE5006E1488 /* VideoPlayListPlugin.swift in Sources */,
498CF2A92B63AABE0009793E /* bit_reader.c in Sources */,
498CF2972B63AABE0009793E /* encode.c in Sources */,
49389D8C28B0A84500B9DAFD /* PersonalViewController.swift in Sources */,
498CF2992B63AABE0009793E /* cluster.c in Sources */,
498CF2952B63AABE0009793E /* compress_fragment_two_pass.c in Sources */,
49D6A7142C0363F10084A5A7 /* VideoPlayListPlugin.swift in Sources */,
F927ED772610395300EAB8E3 /* DanmakuQueuePool.swift in Sources */,
497CF2382C16EDE5006E1488 /* BVideoInfoPlugin.swift in Sources */,
49A441CD293F6DFD0007606C /* FollowUpsViewController.swift in Sources */,
AEA6AB1928FFF3DD007CE72E /* Settings.swift in Sources */,
498CF2962B63AABE0009793E /* block_splitter.c in Sources */,
494741C6290177BB005D6885 /* UpSpaceViewController.swift in Sources */,
496E5A532C01B1CA0062951B /* BVideoInfoPlugin.swift in Sources */,
496E5A552C01CDBB0062951B /* DebugPlugin.swift in Sources */,
49DA01A0296C466C00EEAE15 /* AVInfoPanelCollectionViewThumbnailCell+Hook.swift in Sources */,
492731EE29096677005F5B0A /* HotViewController.swift in Sources */,
@ -932,7 +972,6 @@
49D39F28263AD40000F14497 /* WebRequest.swift in Sources */,
498DB1DF291BC24700F95607 /* BMaskProvider.swift in Sources */,
494741C029002797005D6885 /* UserDefault+..swift in Sources */,
F99D28E12619591300F8E66A /* CommonPlayerViewController.swift in Sources */,
AE4889B228FE55DA00E8C5CD /* FavoriteViewController.swift in Sources */,
F9B57356260F5F7400771ED5 /* LivePlayerViewController.swift in Sources */,
498CF29F2B63AABE0009793E /* static_dict.c in Sources */,
@ -950,6 +989,7 @@
F9171D6429010DF1002868C7 /* FeedCollectionViewCell.swift in Sources */,
F927ED902610A5E900EAB8E3 /* CookieManager.swift in Sources */,
F927ED792610395400EAB8E3 /* DanmakuCellModel.swift in Sources */,
497CF2372C16EDE5006E1488 /* BVideoClipsPlugin.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -1190,6 +1230,14 @@
minimumVersion = 1.0.0;
};
};
2806E51C2C1593A000164C10 /* XCRemoteSwiftPackageReference "LookinServer" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/QMUI/LookinServer/";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 1.2.8;
};
};
49078E45291BEA2400F556BD /* XCRemoteSwiftPackageReference "PocketSVG" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/pocketsvg/PocketSVG.git";
@ -1286,6 +1334,11 @@
package = 0A41EE172A630FEA0066444C /* XCRemoteSwiftPackageReference "swift-protobuf" */;
productName = SwiftProtobuf;
};
2806E51D2C1593A000164C10 /* LookinServer */ = {
isa = XCSwiftPackageProductDependency;
package = 2806E51C2C1593A000164C10 /* XCRemoteSwiftPackageReference "LookinServer" */;
productName = LookinServer;
};
49078E46291BEA2400F556BD /* PocketSVG */ = {
isa = XCSwiftPackageProductDependency;
package = 49078E45291BEA2400F556BD /* XCRemoteSwiftPackageReference "PocketSVG" */;

View File

@ -1,4 +1,5 @@
{
"originHash" : "45c5c9267c8c84275fa647f629e1033e605b18efefc17e781b907af506eb5385",
"pins" : [
{
"identity" : "alamofire",
@ -45,6 +46,15 @@
"version" : "6.3.1"
}
},
{
"identity" : "lookinserver",
"kind" : "remoteSourceControl",
"location" : "https://github.com/QMUI/LookinServer/",
"state" : {
"revision" : "e553d1b689d147817dc54ad5c28fcff71e860101",
"version" : "1.2.8"
}
},
{
"identity" : "marqueelabel",
"kind" : "remoteSourceControl",
@ -118,5 +128,5 @@
}
}
],
"version" : 2
"version" : 3
}

View File

@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder.AppleTV.Storyboard" version="3.0" toolsVersion="21507" targetRuntime="AppleTV" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<document type="com.apple.InterfaceBuilder.AppleTV.Storyboard" version="3.0" toolsVersion="32700.99.1234" targetRuntime="AppleTV" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="appleTV" appearance="dark"/>
<dependencies>
<deployment identifier="tvOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21505"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22684"/>
<capability name="Named colors" minToolsVersion="9.0"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
@ -646,15 +646,15 @@
<constraints>
<constraint firstAttribute="height" constant="360" id="dCt-K6-G5Q"/>
</constraints>
<collectionViewFlowLayout key="collectionViewLayout" scrollDirection="horizontal" minimumLineSpacing="10" minimumInteritemSpacing="10" id="q3p-JN-DhJ">
<collectionViewFlowLayout key="collectionViewLayout" minimumLineSpacing="10" minimumInteritemSpacing="10" id="q3p-JN-DhJ">
<size key="itemSize" width="582" height="360"/>
<size key="headerReferenceSize" width="0.0" height="0.0"/>
<size key="footerReferenceSize" width="0.0" height="0.0"/>
<inset key="sectionInset" minX="0.0" minY="0.0" maxX="30" maxY="0.0"/>
<inset key="sectionInset" minX="60" minY="0.0" maxX="60" maxY="0.0"/>
</collectionViewFlowLayout>
<cells>
<collectionViewCell opaque="NO" multipleTouchEnabled="YES" contentMode="center" reuseIdentifier="ReplyCell" id="gxq-A8-Ahf" customClass="ReplyCell" customModule="BilibiliLive" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="582" height="360"/>
<rect key="frame" x="60" y="0.0" width="582" height="360"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<collectionViewCellContentView key="contentView" opaque="NO" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="91g-YW-NBL">
<rect key="frame" x="0.0" y="0.0" width="582" height="360"/>
@ -801,6 +801,7 @@
<outlet property="playButton" destination="lfB-VX-ver" id="9XU-MT-KOf"/>
<outlet property="playCountLabel" destination="ahl-Fa-Qpg" id="q6V-be-wNj"/>
<outlet property="recommandCollectionView" destination="nw1-cq-9yk" id="w8m-bc-mWQ"/>
<outlet property="repliesCollectionViewHeightConstraints" destination="dCt-K6-G5Q" id="ZfP-oq-I5s"/>
<outlet property="replysCollectionView" destination="zj0-CY-pLt" id="8h4-LO-bUs"/>
<outlet property="scrollView" destination="Dm8-CL-kxU" id="R6I-to-71J"/>
<outlet property="titleLabel" destination="kwE-Zo-p5T" id="3Fx-I9-hrS"/>
@ -936,7 +937,7 @@
<color red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
<systemColor name="secondaryLabelColor">
<color red="0.0" green="0.0" blue="0.0" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
<color white="0.0" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
</resources>
</document>

View File

@ -1,266 +1,60 @@
//
// CommonPlayerViewController.swift
// NewCommonPlayerViewController.swift
// BilibiliLive
//
// Created by Etan Chen on 2021/4/4.
// Created by yicheng on 2024/5/23.
//
import AVKit
import Kingfisher
import UIKit
import Vision
protocol MaskProvider: AnyObject {
func getMask(for time: CMTime, frame: CGRect, onGet: @escaping (CALayer) -> Void)
func needVideoOutput() -> Bool
func setVideoOutout(ouput: AVPlayerItemVideoOutput)
func preferFPS() -> Int
}
class CommonPlayerViewController: AVPlayerViewController {
let danMuView = DanmakuView()
var allowChangeSpeed = true
var playerStartPos: Int?
private var retryCount = 0
private let maxRetryCount = 3
private var observer: NSKeyValueObservation?
class CommonPlayerViewController: UIViewController {
private let playerVC = AVPlayerViewController()
private var activePlugins = [CommonPlayerPlugin]()
private var observations = Set<NSKeyValueObservation>()
private var rateObserver: NSKeyValueObservation?
private var debugView: UILabel?
var maskProvider: MaskProvider?
var playerItem: AVPlayerItem? {
didSet {
if let playerItem = playerItem {
removeObservarPlayerItem()
observePlayerItem(playerItem)
if let playerInfo = playerInfo {
playerItem.externalMetadata = playerInfo
}
}
}
}
override var player: AVPlayer? {
didSet {
if let player = player {
rateObserver = player.observe(\.rate, options: [.old, .new]) {
[weak self] player, _ in
guard let self = self else { return }
playerRateDidChange(player: player)
}
danMuView.play()
} else {
rateObserver = nil
}
}
}
var videoOutput: AVPlayerItemVideoOutput?
private var playerInfo: [AVMetadataItem]?
deinit {
stopDebug()
}
private var statusObserver: NSKeyValueObservation?
private var isEnd = false
override func viewDidLoad() {
super.viewDidLoad()
appliesPreferredDisplayCriteriaAutomatically = Settings.contentMatch
allowsPictureInPicturePlayback = true
delegate = self
initDanmuView()
setupPlayerMenu()
}
addChild(playerVC)
view.addSubview(playerVC.view)
playerVC.didMove(toParent: self)
playerVC.view.snp.makeConstraints { $0.edges.equalToSuperview() }
playerVC.allowsPictureInPicturePlayback = true
playerVC.delegate = self
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
danMuView.recaculateTracks()
danMuView.paddingTop = 5
danMuView.trackHeight = 50
danMuView.displayArea = Settings.danmuArea.percent
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillAppear(animated)
danMuView.stop()
}
func extraInfoForPlayerError() -> String {
return ""
}
func playerStatusDidChange() {
Logger.debug("player status: \(player?.currentItem?.status.rawValue ?? -1)")
switch player?.currentItem?.status {
case .readyToPlay:
if maskProvider?.needVideoOutput() == true {
setUpOutput()
let playerObservation = playerVC.observe(\.player) { [weak self] vc, obs in
if let oldPlayer = obs.oldValue, let oldPlayer {
self?.activePlugins.forEach { $0.playerDidCleanUp(player: oldPlayer) }
}
startPlay()
case .failed:
removeObservarPlayerItem()
Logger.debug(player?.currentItem?.error ?? "no error")
Logger.debug(player?.currentItem?.errorLog() ?? "no error log")
if retryCount < maxRetryCount, !retryPlay() {
let log = playerItem?.errorLog()
let errorLogData = log?.extendedLogData() ?? Data()
var str = String(data: errorLogData, encoding: .utf8) ?? ""
str = str.split(separator: "\n").dropFirst(4).joined()
showErrorAlertAndExit(title: "播放器失败", message: str + extraInfoForPlayerError())
}
retryCount += 1
default:
break
self?.playerDidChange(player: vc.player)
}
observations.insert(playerObservation)
activePlugins.forEach { $0.playerDidLoad(playerVC: playerVC) }
}
func playerRateDidChange(player: AVPlayer) {}
@MainActor func setPlayerInfo(title: String?, subTitle: String?, desp: String?, pic: URL?) {
let desp = desp?.components(separatedBy: "\n").joined(separator: " ")
let mapping: [AVMetadataIdentifier: Any?] = [
.commonIdentifierTitle: title,
.iTunesMetadataTrackSubTitle: subTitle,
.commonIdentifierDescription: desp,
]
let meta = mapping.compactMap { createMetadataItem(for: $0, value: $1) }
playerInfo = meta
playerItem?.externalMetadata = meta
if let pic = pic {
let resource = Kingfisher.ImageResource(downloadURL: pic)
KingfisherManager.shared.retrieveImage(with: resource) {
[weak self] result in
guard let self = self,
let data = try? result.get().image.pngData(),
let item = self.createMetadataItem(for: .commonIdentifierArtwork, value: data)
else { return }
self.playerInfo?.removeAll { $0.identifier == .commonIdentifierArtwork }
self.playerInfo?.append(item)
self.playerItem?.externalMetadata = self.playerInfo ?? []
}
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
activePlugins.forEach { $0.playerDidDismiss(playerVC: playerVC) }
}
func createMetadataItem(for identifier: AVMetadataIdentifier,
value: Any?) -> AVMetadataItem?
{
if value == nil { return nil }
let item = AVMutableMetadataItem()
item.identifier = identifier
item.value = value as? NSCopying & NSObjectProtocol
// Specify "und" to indicate an undefined language.
item.extendedLanguageTag = "und"
return item.copy() as? AVMetadataItem
override var preferredFocusEnvironments: [UIFocusEnvironment] {
return [playerVC.view]
}
private func setupPlayerMenu() {
var menus = [UIMenuElement]()
let danmuImage = UIImage(systemName: "list.bullet.rectangle.fill")
let danmuImageDisable = UIImage(systemName: "list.bullet.rectangle")
let danmuAction = UIAction(title: "Show Danmu", image: danMuView.isHidden ? danmuImageDisable : danmuImage) {
[weak self] action in
guard let self = self else { return }
Settings.defaultDanmuStatus.toggle()
self.danMuView.isHidden.toggle()
action.image = self.danMuView.isHidden ? danmuImageDisable : danmuImage
}
menus.append(danmuAction)
let danmuDurationMenu = UIMenu(title: "弹幕展示时长", options: [.displayInline, .singleSelection], children: [4, 6, 8].map { dur in
UIAction(title: "\(dur)", state: dur == Settings.danmuDuration ? .on : .off) { _ in Settings.danmuDuration = dur }
})
let danmuAILevelMenu = UIMenu(title: "弹幕屏蔽等级", options: [.displayInline, .singleSelection], children: [Int32](1...10).map { level in
UIAction(title: "\(level)", state: level == Settings.danmuAILevel ? .on : .off) { _ in Settings.danmuAILevel = level }
})
let danmuSettingMenu = UIMenu(title: "弹幕设置", image: UIImage(systemName: "keyboard.badge.ellipsis"), children: [danmuDurationMenu, danmuAILevelMenu])
menus.append(danmuSettingMenu)
let debugEnableImage = UIImage(systemName: "terminal.fill")
let debugDisableImage = UIImage(systemName: "terminal")
let debugAction = UIAction(title: "Debug", image: debugEnable ? debugEnableImage : debugDisableImage) {
[weak self] action in
guard let self = self else { return }
if self.debugEnable {
self.stopDebug()
action.image = debugDisableImage
} else {
action.image = debugEnableImage
self.startDebug()
}
}
if allowChangeSpeed {
// Create and images.
let loopImage = UIImage(systemName: "infinity")
let gearImage = UIImage(systemName: "gearshape")
// Create an action to enable looping playback.
let loopAction = UIAction(title: "循环播放", image: loopImage, state: Settings.loopPlay ? .on : .off) {
action in
action.state = (action.state == .off) ? .on : .off
Settings.loopPlay = action.state == .on
}
let speedActions = PlaySpeed.blDefaults.map { playSpeed in
UIAction(title: playSpeed.name, state: player?.rate ?? 1 == playSpeed.value ? .on : .off) { [weak self] _ in
self?.player?.currentItem?.audioTimePitchAlgorithm = .timeDomain
self?.selectSpeed(AVPlaybackSpeed(rate: playSpeed.value, localizedName: playSpeed.name))
self?.danMuView.playingSpeed = playSpeed.value
}
}
let playSpeedMenu = UIMenu(title: "播放速度", options: [.displayInline, .singleSelection], children: speedActions)
let menu = UIMenu(title: "播放设置", image: gearImage, children: [playSpeedMenu, loopAction, debugAction])
menus.append(menu)
} else {
menus.append(debugAction)
}
transportBarCustomMenuItems = menus
func addPlugin(plugin: CommonPlayerPlugin) {
plugin.addViewToPlayerOverlay(container: playerVC.contentOverlayView!)
activePlugins.append(plugin)
plugin.playerDidLoad(playerVC: playerVC)
}
private func removeObservarPlayerItem() {
NotificationCenter.default.removeObserver(self, name: .AVPlayerItemDidPlayToEndTime, object: nil)
func removePlugin(plugin: CommonPlayerPlugin) {
activePlugins.removeAll { $0 == plugin }
}
private func observePlayerItem(_ playerItem: AVPlayerItem) {
observer = playerItem.observe(\.status, options: [.new, .old]) {
[weak self] _, _ in
self?.playerStatusDidChange()
}
NotificationCenter.default.addObserver(self,
selector: #selector(playerDidFinishPlaying),
name: .AVPlayerItemDidPlayToEndTime,
object: playerItem)
}
func setupMask() {
guard let maskProvider else { return }
// danMuView.backgroundColor = UIColor.red.withAlphaComponent(0.5)
Logger.info("mask provider is \(maskProvider)")
let interval = CMTime(seconds: 1.0 / CGFloat(maskProvider.preferFPS()),
preferredTimescale: CMTimeScale(NSEC_PER_SEC))
player?.addPeriodicTimeObserver(forInterval: interval, queue: .main, using: {
[weak self, weak maskProvider] time in
guard let self else { return }
guard self.danMuView.isHidden == false else { return }
maskProvider?.getMask(for: time, frame: self.danMuView.frame) {
maskLayer in
self.danMuView.layer.mask = maskLayer
}
})
}
func retryPlay() -> Bool {
return false
}
@objc private func playerDidFinishPlaying() {
playDidEnd()
}
func playDidEnd() {}
func playerDidEnd(player: AVPlayer) {}
func showErrorAlertAndExit(title: String = "播放失败", message: String = "未知错误") {
let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
@ -271,111 +65,74 @@ class CommonPlayerViewController: AVPlayerViewController {
alertController.addAction(actionOk)
present(alertController, animated: true, completion: nil)
}
}
private func startPlay() {
guard player?.rate == 0 && player?.error == nil else { return }
if let playerStartPos = playerStartPos {
player?.seek(to: CMTime(seconds: Double(playerStartPos), preferredTimescale: 1), toleranceBefore: .zero, toleranceAfter: .zero)
extension CommonPlayerViewController {
private func playerDidChange(player: AVPlayer?) {
if let player {
activePlugins.forEach { $0.playerDidChange(player: player) }
rateObserver = player.observe(\.rate, options: [.old, .new]) {
[weak self] _player, obs in
DispatchQueue.main.async { [weak self] in
self?.playerRateDidChange(player: player)
}
}
if let playItem = player.currentItem {
observePlayerItem(playItem)
}
var menus = [UIMenuElement]()
activePlugins.forEach {
let newMenus = $0.addMenuItems(current: &menus)
menus.append(contentsOf: newMenus)
}
playerVC.transportBarCustomMenuItems = menus
} else {
rateObserver = nil
}
player?.play()
}
private func fetchDebugInfo() -> String {
let bitrateStr: (Double) -> String = {
bit in
String(format: "%.2fMbps", bit / 1024.0 / 1024.0)
}
guard let player else { return "Player no init" }
var logs = """
\(additionDebugInfo())
time control status: \(player.timeControlStatus.rawValue) \(player.reasonForWaitingToPlay?.rawValue ?? "")
player status:\(player.status.rawValue)
"""
guard let log = player.currentItem?.accessLog() else { return logs }
guard let item = log.events.last else { return logs }
let uri = item.uri ?? ""
let addr = item.serverAddress ?? ""
let changes = item.numberOfServerAddressChanges
let dropped = item.numberOfDroppedVideoFrames
let stalls = item.numberOfStalls
let averageAudioBitrate = item.averageAudioBitrate
let averageVideoBitrate = item.averageVideoBitrate
let indicatedBitrate = item.indicatedBitrate
let observedBitrate = item.observedBitrate
logs += """
uri:\(uri), ip:\(addr), change:\(changes)
drop:\(dropped) stalls:\(stalls)
bitrate audio:\(bitrateStr(averageAudioBitrate)), video: \(bitrateStr(averageVideoBitrate))
observedBitrate:\(bitrateStr(observedBitrate))
indicatedAverageBitrate:\(bitrateStr(indicatedBitrate))
maskProvider: \(String(describing: maskProvider))
"""
return logs
}
func additionDebugInfo() -> String { return "" }
var debugTimer: Timer?
var debugEnable: Bool { debugTimer?.isValid ?? false }
private func startDebug() {
if debugView == nil {
debugView = UILabel()
debugView?.backgroundColor = UIColor.black.withAlphaComponent(0.8)
debugView?.textColor = UIColor.white
view.addSubview(debugView!)
debugView?.numberOfLines = 0
debugView?.font = UIFont.systemFont(ofSize: 26)
debugView?.snp.makeConstraints { make in
make.top.equalToSuperview().offset(12)
make.right.equalToSuperview().offset(-12)
make.width.equalTo(800)
private func playerRateDidChange(player: AVPlayer) {
if player.rate > 0 {
activePlugins.forEach { $0.playerDidStart(player: player) }
} else if player.rate == 0 {
if !isEnd {
activePlugins.forEach { $0.playerDidPause(player: player) }
}
}
debugView?.isHidden = false
debugTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
let info = self?.fetchDebugInfo()
self?.debugView?.text = info
}
private func observePlayerItem(_ playerItem: AVPlayerItem) {
statusObserver = playerItem.observe(\.status, options: [.new, .old]) {
[weak self] item, _ in
guard let self, let player = playerVC.player else { return }
switch item.status {
case .readyToPlay:
isEnd = false
activePlugins.forEach { $0.playerWillStart(player: player) }
player.play()
case .failed:
activePlugins.forEach { $0.playerDidFail(player: player) }
default:
break
}
}
NotificationCenter.default.removeObserver(self, name: .AVPlayerItemDidPlayToEndTime, object: nil)
NotificationCenter.default.addObserver(forName: .AVPlayerItemDidPlayToEndTime, object: playerItem, queue: .main) { [weak self] note in
guard let self, let player = playerVC.player else { return }
isEnd = true
activePlugins.forEach { $0.playerDidEnd(player: player) }
playerDidEnd(player: player)
}
}
private func stopDebug() {
debugTimer?.invalidate()
debugTimer = nil
debugView?.isHidden = true
}
private func initDanmuView() {
view.addSubview(danMuView)
danMuView.accessibilityLabel = "danmuView"
danMuView.makeConstraintsToBindToSuperview()
danMuView.isHidden = !Settings.defaultDanmuStatus
}
func setUpOutput() {
guard videoOutput == nil, let videoItem = player?.currentItem else { return }
let pixelBuffAttributes = [
kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange,
]
let videoOutput = AVPlayerItemVideoOutput(pixelBufferAttributes: pixelBuffAttributes)
videoItem.add(videoOutput)
self.videoOutput = videoOutput
maskProvider?.setVideoOutout(ouput: videoOutput)
}
func ensureDanmuViewFront() {
view.bringSubviewToFront(danMuView)
danMuView.play()
}
}
extension CommonPlayerViewController: AVPlayerViewControllerDelegate {
@objc func playerViewControllerShouldDismiss(_ playerViewController: AVPlayerViewController) -> Bool {
if let presentedViewController = UIViewController.topMostViewController() as? AVPlayerViewController,
presentedViewController == playerViewController
if let presentedViewController = UIViewController.topMostViewController() as? CommonPlayerViewController,
presentedViewController.playerVC == playerViewController
{
return true
dismiss(animated: true)
return false
}
return false
}
@ -384,40 +141,37 @@ extension CommonPlayerViewController: AVPlayerViewControllerDelegate {
return true
}
func playerViewControllerWillStartPictureInPicture(_ playerViewController: AVPlayerViewController) {
PipRecorder.shared.playingPipViewController.append(self)
}
func playerViewControllerDidStopPictureInPicture(_ playerViewController: AVPlayerViewController) {
PipRecorder.shared.playingPipViewController.removeAll { $0.playerVC == playerViewController }
}
@objc func playerViewController(_ playerViewController: AVPlayerViewController,
restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void)
{
let presentedViewController = UIViewController.topMostViewController()
if presentedViewController is AVPlayerViewController {
guard let containerPlayer = PipRecorder.shared.playingPipViewController.first(where: { $0.playerVC == playerViewController }) else {
completionHandler(false)
return
}
if presentedViewController is CommonPlayerViewController {
let parent = presentedViewController.presentingViewController
presentedViewController.dismiss(animated: false) {
parent?.present(playerViewController, animated: false)
parent?.present(containerPlayer, animated: false)
completionHandler(true)
(playerViewController as? CommonPlayerViewController)?.ensureDanmuViewFront()
}
} else {
presentedViewController.present(playerViewController, animated: false) {
presentedViewController.present(containerPlayer, animated: false) {
completionHandler(true)
(playerViewController as? CommonPlayerViewController)?.ensureDanmuViewFront()
}
}
}
}
struct PlaySpeed {
var name: String
var value: Float
}
extension PlaySpeed: Equatable {
static let `default` = PlaySpeed(name: "1X", value: 1)
static let blDefaults = [
PlaySpeed(name: "0.5X", value: 0.5),
PlaySpeed(name: "0.75X", value: 0.75),
PlaySpeed(name: "1X", value: 1),
PlaySpeed(name: "1.25X", value: 1.25),
PlaySpeed(name: "1.5X", value: 1.5),
PlaySpeed(name: "2X", value: 2),
]
class PipRecorder {
static let shared = PipRecorder()
var playingPipViewController = [CommonPlayerViewController]()
}
}

View File

@ -1,177 +0,0 @@
//
// NewCommonPlayerViewController.swift
// BilibiliLive
//
// Created by yicheng on 2024/5/23.
//
import AVKit
import UIKit
class NewCommonPlayerViewController: UIViewController {
private let playerVC = AVPlayerViewController()
private var activePlugins = [CommonPlayerPlugin]()
private var observations = Set<NSKeyValueObservation>()
private var rateObserver: NSKeyValueObservation?
private var statusObserver: NSKeyValueObservation?
private var isEnd = false
override func viewDidLoad() {
super.viewDidLoad()
addChild(playerVC)
view.addSubview(playerVC.view)
playerVC.didMove(toParent: self)
playerVC.view.snp.makeConstraints { $0.edges.equalToSuperview() }
playerVC.allowsPictureInPicturePlayback = true
playerVC.delegate = self
let playerObservation = playerVC.observe(\.player) { [weak self] vc, obs in
if let oldPlayer = obs.oldValue, let oldPlayer {
self?.activePlugins.forEach { $0.playerDidCleanUp(player: oldPlayer) }
}
self?.playerDidChange(player: vc.player)
}
observations.insert(playerObservation)
activePlugins.forEach { $0.playerDidLoad(playerVC: playerVC) }
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
activePlugins.forEach { $0.playerDidDismiss(playerVC: playerVC) }
}
override var preferredFocusEnvironments: [UIFocusEnvironment] {
return [playerVC.view]
}
func addPlugin(plugin: CommonPlayerPlugin) {
plugin.addViewToPlayerOverlay(container: playerVC.contentOverlayView!)
activePlugins.append(plugin)
plugin.playerDidLoad(playerVC: playerVC)
}
func removePlugin(plugin: CommonPlayerPlugin) {
activePlugins.removeAll { $0 == plugin }
}
func playerDidEnd(player: AVPlayer) {}
func showErrorAlertAndExit(title: String = "播放失败", message: String = "未知错误") {
let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
let actionOk = UIAlertAction(title: "OK", style: .default) {
[weak self] _ in
self?.dismiss(animated: true, completion: nil)
}
alertController.addAction(actionOk)
present(alertController, animated: true, completion: nil)
}
}
extension NewCommonPlayerViewController {
private func playerDidChange(player: AVPlayer?) {
if let player {
activePlugins.forEach { $0.playerDidChange(player: player) }
rateObserver = player.observe(\.rate, options: [.old, .new]) {
[weak self] _player, obs in
DispatchQueue.main.async { [weak self] in
self?.playerRateDidChange(player: player)
}
}
if let playItem = player.currentItem {
observePlayerItem(playItem)
}
var menus = [UIMenuElement]()
activePlugins.forEach {
let newMenus = $0.addMenuItems(current: &menus)
menus.append(contentsOf: newMenus)
}
playerVC.transportBarCustomMenuItems = menus
} else {
rateObserver = nil
}
}
private func playerRateDidChange(player: AVPlayer) {
if player.rate > 0 {
activePlugins.forEach { $0.playerDidStart(player: player) }
} else if player.rate == 0 {
if !isEnd {
activePlugins.forEach { $0.playerDidPause(player: player) }
}
}
}
private func observePlayerItem(_ playerItem: AVPlayerItem) {
statusObserver = playerItem.observe(\.status, options: [.new, .old]) {
[weak self] item, _ in
guard let self, let player = playerVC.player else { return }
switch item.status {
case .readyToPlay:
isEnd = false
activePlugins.forEach { $0.playerWillStart(player: player) }
player.play()
case .failed:
activePlugins.forEach { $0.playerDidFail(player: player) }
default:
break
}
}
NotificationCenter.default.removeObserver(self, name: .AVPlayerItemDidPlayToEndTime, object: nil)
NotificationCenter.default.addObserver(forName: .AVPlayerItemDidPlayToEndTime, object: playerItem, queue: .main) { [weak self] note in
guard let self, let player = playerVC.player else { return }
isEnd = true
activePlugins.forEach { $0.playerDidEnd(player: player) }
playerDidEnd(player: player)
}
}
}
extension NewCommonPlayerViewController: AVPlayerViewControllerDelegate {
@objc func playerViewControllerShouldDismiss(_ playerViewController: AVPlayerViewController) -> Bool {
if let presentedViewController = UIViewController.topMostViewController() as? NewCommonPlayerViewController,
presentedViewController.playerVC == playerViewController
{
dismiss(animated: true)
return false
}
return false
}
@objc func playerViewControllerShouldAutomaticallyDismissAtPictureInPictureStart(_: AVPlayerViewController) -> Bool {
return true
}
func playerViewControllerWillStartPictureInPicture(_ playerViewController: AVPlayerViewController) {
PipRecorder.shared.playingPipViewController.append(self)
}
func playerViewControllerDidStopPictureInPicture(_ playerViewController: AVPlayerViewController) {
PipRecorder.shared.playingPipViewController.removeAll { $0.playerVC == playerViewController }
}
@objc func playerViewController(_ playerViewController: AVPlayerViewController,
restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void)
{
let presentedViewController = UIViewController.topMostViewController()
guard let containerPlayer = PipRecorder.shared.playingPipViewController.first(where: { $0.playerVC == playerViewController }) else {
completionHandler(false)
return
}
if presentedViewController is NewCommonPlayerViewController {
let parent = presentedViewController.presentingViewController
presentedViewController.dismiss(animated: false) {
parent?.present(containerPlayer, animated: false)
completionHandler(true)
}
} else {
presentedViewController.present(containerPlayer, animated: false) {
completionHandler(true)
}
}
}
class PipRecorder {
static let shared = PipRecorder()
var playingPipViewController = [NewCommonPlayerViewController]()
}
}

View File

@ -16,10 +16,6 @@ protocol DanmuProviderProtocol {
}
class DanmuViewPlugin: NSObject {
var showDanmu = Settings.defaultDanmuStatus {
didSet { danMuView.isHidden = !showDanmu }
}
let danMuView = DanmakuView()
init(provider: DanmuProviderProtocol) {
@ -30,6 +26,13 @@ class DanmuViewPlugin: NSObject {
.sink { [weak self] in
self?.shoot($0)
}.store(in: &cancellable)
Defaults.shared.$showDanmu
.receive(on: DispatchQueue.main)
.sink {
[weak self] in
self?.danMuView.isHidden = !$0
}.store(in: &cancellable)
}
private let danmuProvider: DanmuProviderProtocol
@ -49,7 +52,7 @@ extension DanmuViewPlugin: CommonPlayerPlugin {
player.addPeriodicTimeObserver(forInterval: CMTime(seconds: 1, preferredTimescale: 1),
queue: DispatchQueue.global()) { [weak self] time in
guard let self else { return }
if danMuView.isHidden { return }
if !Defaults.shared.showDanmu { return }
let seconds = time.seconds
danmuProvider.playerTimeChange(time: seconds)
}
@ -66,7 +69,6 @@ extension DanmuViewPlugin: CommonPlayerPlugin {
danMuView.makeConstraintsToBindToSuperview()
danMuView.setNeedsLayout()
danMuView.layoutIfNeeded()
danMuView.isHidden = showDanmu
danMuView.paddingTop = 5
danMuView.trackHeight = 50
danMuView.displayArea = Settings.danmuArea.percent
@ -85,11 +87,9 @@ extension DanmuViewPlugin: CommonPlayerPlugin {
let danmuImage = UIImage(systemName: "list.bullet.rectangle.fill")
let danmuImageDisable = UIImage(systemName: "list.bullet.rectangle")
let danmuAction = UIAction(title: "Show Danmu", image: danMuView.isHidden ? danmuImageDisable : danmuImage) {
[weak self] action in
guard let self = self else { return }
Settings.defaultDanmuStatus.toggle()
self.danMuView.isHidden.toggle()
action.image = self.danMuView.isHidden ? danmuImageDisable : danmuImage
action in
Defaults.shared.showDanmu.toggle()
action.image = Defaults.shared.showDanmu ? danmuImage : danmuImageDisable
}
let danmuDurationMenu = UIMenu(title: "弹幕展示时长", options: [.displayInline, .singleSelection], children: [4, 6, 8].map { dur in
UIAction(title: "\(dur)", state: dur == Settings.danmuDuration ? .on : .off) { _ in Settings.danmuDuration = dur }

View File

@ -42,3 +42,21 @@ class SpeedChangerPlugin: NSObject, CommonPlayerPlugin {
return [menu]
}
}
struct PlaySpeed {
var name: String
var value: Float
}
extension PlaySpeed: Equatable {
static let `default` = PlaySpeed(name: "1X", value: 1)
static let blDefaults = [
PlaySpeed(name: "0.5X", value: 0.5),
PlaySpeed(name: "0.75X", value: 0.75),
PlaySpeed(name: "1X", value: 1),
PlaySpeed(name: "1.25X", value: 1.25),
PlaySpeed(name: "1.5X", value: 1.5),
PlaySpeed(name: "2X", value: 2),
]
}

View File

@ -5,7 +5,9 @@
// Created by whw on 2022/10/19.
//
import Combine
import Foundation
import SwiftUI
enum FeedDisplayStyle: Codable, CaseIterable {
case large
@ -17,6 +19,13 @@ enum FeedDisplayStyle: Codable, CaseIterable {
}
}
class Defaults {
static let shared = Defaults()
private init() {}
@Published(key: "Settings.danmuStatus") var showDanmu = true
}
enum Settings {
@UserDefaultCodable("Settings.displayStyle", defaultValue: .normal)
static var displayStyle: FeedDisplayStyle
@ -45,9 +54,6 @@ enum Settings {
@UserDefault("Settings.preferAvc", defaultValue: false)
static var preferAvc: Bool
@UserDefault("Settings.defaultDanmuStatus", defaultValue: true)
static var defaultDanmuStatus: Bool
@UserDefault("Settings.danmuMask", defaultValue: true)
static var danmuMask: Bool

View File

@ -0,0 +1,14 @@
//
// MaskProvider.swift
// BilibiliLive
//
// Created by yicheng on 2024/6/10.
//
import AVKit
protocol MaskProvider: AnyObject {
func getMask(for time: CMTime, frame: CGRect, onGet: @escaping (CALayer) -> Void)
func needVideoOutput() -> Bool
func setVideoOutout(ouput: AVPlayerItemVideoOutput)
func preferFPS() -> Int
}

View File

@ -1,52 +0,0 @@
//
// NewVideoPlayerViewController.swift
// BilibiliLive
//
// Created by yicheng on 2024/5/23.
//
import AVKit
import Combine
import UIKit
class NewVideoPlayerViewController: NewCommonPlayerViewController {
var data: VideoDetail?
var nextProvider: VideoNextProvider?
init(playInfo: PlayInfo) {
viewModel = NewVideoPlayerViewModel(playInfo: playInfo)
super.init(nibName: nil, bundle: nil)
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private let viewModel: NewVideoPlayerViewModel
private var cancelable = Set<AnyCancellable>()
override func viewDidLoad() {
super.viewDidLoad()
viewModel.nextProvider = nextProvider
viewModel.onPluginReady.receive(on: DispatchQueue.main).sink { [weak self] completion in
switch completion {
case let .failure(err):
self?.showErrorAlertAndExit(message: err)
default:
break
}
} receiveValue: { [weak self] plugins in
plugins.forEach { self?.addPlugin(plugin: $0) }
}.store(in: &cancelable)
viewModel.onPluginRemove.sink { [weak self] in
self?.removePlugin(plugin: $0)
}.store(in: &cancelable)
viewModel.onExit = { [weak self] in
self?.dismiss(animated: true)
}
Task {
await viewModel.load()
}
}
}

View File

@ -21,7 +21,7 @@ struct PlayerDetailData {
var videoPlayURLInfo: VideoPlayURLInfo
}
class NewVideoPlayerViewModel {
class VideoPlayerViewModel {
var onPluginReady = PassthroughSubject<[CommonPlayerPlugin], String>()
var onPluginRemove = PassthroughSubject<CommonPlayerPlugin, Never>()
var onExit: (() -> Void)?
@ -78,7 +78,19 @@ class NewVideoPlayerViewModel {
var clipInfos: [VideoPlayURLInfo.ClipInfo]?
if playInfo.isBangumi {
playData = try await WebRequest.requestPcgPlayUrl(aid: aid, cid: cid)
do {
playData = try await WebRequest.requestPcgPlayUrl(aid: aid, cid: cid)
} catch let err as RequestError {
if case let .statusFail(code, _) = err,
code == -404 || code == -10403,
let data = try await fetchAreaLimitPcgVideoData()
{
playData = data
} else {
throw err
}
}
clipInfos = playData.clip_info_list
} else {
playData = try await WebRequest.requestPlayUrl(aid: aid, cid: cid)
@ -97,15 +109,6 @@ class NewVideoPlayerViewModel {
} catch let err {
if case let .statusFail(code, message) = err as? RequestError {
if code == -404 || code == -10403 {
//
// do {
// if let ok = try await fetchAreaLimitVideoData(), ok {
// return
// }
// } catch let err {
// }
}
throw "\(code) \(message),可能需要大会员"
} else if await infoReq?.is_upower_exclusive == true {
throw "该视频为充电专属视频 \(err)"
@ -181,3 +184,56 @@ class NewVideoPlayerViewModel {
return plugins
}
}
//
extension VideoPlayerViewModel {
private func fetchAreaLimitPcgVideoData() async throws -> VideoPlayURLInfo? {
guard Settings.areaLimitUnlock else { return nil }
guard let epid = playInfo.epid, epid > 0 else { return nil }
let season = try await WebRequest.requestBangumiSeasonView(epid: epid)
let checkTitle = season.title.contains("") ? season.title : season.series_title
let checkAreaList = parseAreaByTitle(title: checkTitle)
guard !checkAreaList.isEmpty else { return nil }
let playData = try await requestAreaLimitPcgPlayUrl(epid: epid, cid: playInfo.cid!, areaList: checkAreaList)
return playData
}
private func requestAreaLimitPcgPlayUrl(epid: Int, cid: Int, areaList: [String]) async throws -> VideoPlayURLInfo? {
for area in areaList {
do {
return try await WebRequest.requestAreaLimitPcgPlayUrl(epid: epid, cid: cid, area: area)
} catch let err {
if area == areaList.last {
throw err
} else {
print(err)
}
}
}
return nil
}
private func parseAreaByTitle(title: String) -> [String] {
if title.isMatch(pattern: "[仅|僅].*[东南亚|其他]") {
// TODO:
return []
}
var areas: [String] = []
if title.isMatch(pattern: "僅.*台") {
areas.append("tw")
}
if title.isMatch(pattern: "僅.*港") {
areas.append("hk")
}
if areas.isEmpty {
//
return ["tw", "hk"]
} else {
return areas
}
}
}

View File

@ -9,12 +9,26 @@ import AVFoundation
import Foundation
class BUpnpPlugin: NSObject, CommonPlayerPlugin {
let duration: Int?
weak var player: AVPlayer?
init(duration: Int?) {
self.duration = duration
}
func pause() {
player?.pause()
}
func resume() {
player?.play()
}
func seek(to time: TimeInterval) {
player?.seek(to: CMTime(seconds: time, preferredTimescale: 1), toleranceBefore: .zero, toleranceAfter: .zero)
}
func playerWillStart(player: AVPlayer) {
self.player = player
guard let duration else { return }
player.addPeriodicTimeObserver(forInterval: CMTime(seconds: 5, preferredTimescale: 1), queue: .global()) { time in
DispatchQueue.main.async {

View File

@ -0,0 +1,111 @@
//
// Created by Yam on 2024/6/9.
//
import UIKit
class ReplyDetailViewController: UIViewController {
private var titleLabel: UILabel!
private var replyLabel: UILabel!
private var replyCollectionView: UICollectionView!
var reply: Replys.Reply
init(reply: Replys.Reply) {
self.reply = reply
super.init(nibName: nil, bundle: nil)
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
setUpViews()
replyLabel.text = reply.content.message
}
// MARK: - Private
private func setUpViews() {
titleLabel = {
let label = UILabel()
self.view.addSubview(label)
label.font = .boldSystemFont(ofSize: 60)
label.text = "评论"
label.snp.makeConstraints { make in
make.centerX.equalToSuperview()
make.top.equalTo(view.safeAreaLayoutGuide)
}
return label
}()
replyLabel = {
let label = UILabel()
self.view.addSubview(label)
label.numberOfLines = 0
label.font = .preferredFont(forTextStyle: .headline)
label.snp.makeConstraints { make in
make.top.equalTo(self.titleLabel.snp.bottom).offset(60)
make.leading.equalTo(self.view.snp.leadingMargin)
make.trailing.equalTo(self.view.snp.trailingMargin)
}
return label
}()
replyCollectionView = {
let flowLayout = UICollectionViewFlowLayout()
flowLayout.itemSize = CGSize(width: 582, height: 360)
flowLayout.sectionInset = .init(top: 0, left: 60, bottom: 0, right: 60)
flowLayout.minimumLineSpacing = 10
flowLayout.minimumInteritemSpacing = 10
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: flowLayout)
self.view.addSubview(collectionView)
collectionView.dataSource = self
collectionView.delegate = self
collectionView.register(UINib(nibName: ReplyCell.identifier, bundle: nil), forCellWithReuseIdentifier: ReplyCell.identifier)
collectionView.snp.makeConstraints { make in
make.leading.trailing.equalToSuperview()
make.top.equalTo(self.replyLabel.snp.bottom).offset(60)
make.bottom.equalToSuperview()
}
return collectionView
}()
}
}
extension ReplyDetailViewController: UICollectionViewDataSource, UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return reply.replies?.count ?? 0
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ReplyCell.identifier, for: indexPath) as? ReplyCell else {
fatalError("cell not found")
}
guard let reply = reply.replies?[indexPath.row] else {
fatalError("reply not found")
}
cell.config(replay: reply)
return cell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
guard let reply = reply.replies?[indexPath.item] else { return }
let detail = ReplyDetailViewController(reply: reply)
present(detail, animated: true)
}
}

View File

@ -6,6 +6,7 @@
//
import AVKit
import Combine
import Foundation
import UIKit
@ -43,6 +44,7 @@ class VideoDetailViewController: UIViewController {
@IBOutlet var pageCollectionView: UICollectionView!
@IBOutlet var recommandCollectionView: UICollectionView!
@IBOutlet var replysCollectionView: UICollectionView!
@IBOutlet var repliesCollectionViewHeightConstraints: NSLayoutConstraint!
@IBOutlet var ugcCollectionView: UICollectionView!
@IBOutlet var pageView: UIView!
@ -71,6 +73,8 @@ class VideoDetailViewController: UIViewController {
private var allUgcEpisodes = [VideoDetail.Info.UgcSeason.UgcVideoInfo]()
private var subscriptions = [AnyCancellable]()
static func create(aid: Int, cid: Int?, epid: Int? = nil) -> VideoDetailViewController {
let vc = UIStoryboard(name: "Main", bundle: .main).instantiateViewController(identifier: String(describing: self)) as! VideoDetailViewController
vc.aid = aid
@ -126,6 +130,11 @@ class VideoDetailViewController: UIViewController {
focusGuide.bottomAnchor.constraint(equalTo: actionButtonSpaceView.bottomAnchor),
])
focusGuide.preferredFocusEnvironments = [dislikeButton]
replysCollectionView.publisher(for: \.contentSize).sink { [weak self] newSize in
self?.repliesCollectionViewHeightConstraints.constant = newSize.height
self?.view.setNeedsLayout()
}.store(in: &subscriptions)
}
override var preferredFocusedView: UIView? {
@ -147,7 +156,7 @@ class VideoDetailViewController: UIViewController {
} else {
vc.present(self, animated: false) { [weak self] in
guard let self else { return }
let player = NewVideoPlayerViewController(playInfo: PlayInfo(aid: self.aid, cid: self.cid, epid: self.epid, isBangumi: self.isBangumi))
let player = VideoPlayerViewController(playInfo: PlayInfo(aid: self.aid, cid: self.cid, epid: self.epid, isBangumi: self.isBangumi))
self.present(player, animated: true)
}
}
@ -354,7 +363,7 @@ class VideoDetailViewController: UIViewController {
}
@IBAction func actionPlay(_ sender: Any) {
let player = NewVideoPlayerViewController(playInfo: PlayInfo(aid: aid, cid: cid, epid: epid, isBangumi: isBangumi))
let player = VideoPlayerViewController(playInfo: PlayInfo(aid: aid, cid: cid, epid: epid, isBangumi: isBangumi))
player.data = data
if pages.count > 0, let index = pages.firstIndex(where: { $0.cid == cid }) {
let seq = pages.dropFirst(index).map({ PlayInfo(aid: aid, cid: $0.cid, epid: $0.epid, isBangumi: isBangumi) })
@ -456,7 +465,7 @@ extension VideoDetailViewController: UICollectionViewDelegate {
present(player, animated: true, completion: nil)
case replysCollectionView:
guard let reply = replys?.replies?[indexPath.item] else { return }
let detail = ContentDetailViewController.createReply(content: reply.content.message)
let detail = ReplyDetailViewController(reply: reply)
present(detail, animated: true)
case ugcCollectionView:
let video = allUgcEpisodes[indexPath.item]
@ -576,18 +585,6 @@ extension VideoDetailViewController {
}
}
class ReplyCell: UICollectionViewCell {
@IBOutlet var avatarImageView: UIImageView!
@IBOutlet var userNameLabel: UILabel!
@IBOutlet var contenLabel: UILabel!
func config(replay: Replys.Reply) {
avatarImageView.kf.setImage(with: URL(string: replay.member.avatar), options: [.processor(DownsamplingImageProcessor(size: CGSize(width: 80, height: 80))), .processor(RoundCornerImageProcessor(radius: .widthFraction(0.5))), .cacheSerializer(FormatIndicatedCacheSerializer.png)])
userNameLabel.text = replay.member.uname
contenLabel.text = replay.content.message
}
}
class RelatedVideoCell: BLMotionCollectionViewCell {
let titleLabel = MarqueeLabel()
let imageView = UIImageView()

View File

@ -1,16 +1,12 @@
//
// VideoPlayerViewController.swift
// NewVideoPlayerViewController.swift
// BilibiliLive
//
// Created by Etan Chen on 2021/4/4.
// Created by yicheng on 2024/5/23.
//
import Alamofire
import AVFoundation
import AVKit
import Kingfisher
import SwiftyJSON
import SwiftyXMLParser
import Combine
import UIKit
struct PlayInfo {
@ -46,9 +42,11 @@ class VideoNextProvider {
}
class VideoPlayerViewController: CommonPlayerViewController {
var playInfo: PlayInfo
var data: VideoDetail?
var nextProvider: VideoNextProvider?
init(playInfo: PlayInfo) {
self.playInfo = playInfo
viewModel = VideoPlayerViewModel(playInfo: playInfo)
super.init(nibName: nil, bundle: nil)
}
@ -57,389 +55,30 @@ class VideoPlayerViewController: CommonPlayerViewController {
fatalError("init(coder:) has not been implemented")
}
var data: VideoDetail?
var nextProvider: VideoNextProvider?
private var allDanmus = [Danmu]()
private var playingDanmus = [Danmu]()
private var playerDelegate: BilibiliVideoResourceLoaderDelegate?
private let danmuProvider = VideoDanmuProvider()
private var clipInfos: [VideoPlayURLInfo.ClipInfo]?
private var skipAction: UIAction?
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
guard let currentTime = player?.currentTime().seconds, currentTime > 0 else { return }
if let cid = playInfo.cid, cid > 0 {
WebRequest.reportWatchHistory(aid: playInfo.aid, cid: cid, currentTime: Int(currentTime))
}
BiliBiliUpnpDMR.shared.sendStatus(status: .stop)
}
private let viewModel: VideoPlayerViewModel
private var cancelable = Set<AnyCancellable>()
override func viewDidLoad() {
super.viewDidLoad()
viewModel.nextProvider = nextProvider
viewModel.onPluginReady.receive(on: DispatchQueue.main).sink { [weak self] completion in
switch completion {
case let .failure(err):
self?.showErrorAlertAndExit(message: err)
default:
break
}
} receiveValue: { [weak self] plugins in
plugins.forEach { self?.addPlugin(plugin: $0) }
}.store(in: &cancelable)
viewModel.onPluginRemove.sink { [weak self] in
self?.removePlugin(plugin: $0)
}.store(in: &cancelable)
viewModel.onExit = { [weak self] in
self?.dismiss(animated: true)
}
Task {
await initPlayer()
}
danmuProvider.onShowDanmu = {
[weak self] in
self?.danMuView.shoot(danmaku: $0)
}
}
private func initPlayer() async {
if !playInfo.isCidVaild {
do {
playInfo.cid = try await WebRequest.requestCid(aid: playInfo.aid)
} catch let err {
self.showErrorAlertAndExit(message: "请求cid失败,\(err.localizedDescription)")
}
}
await fetchVideoData()
await danmuProvider.initVideo(cid: playInfo.cid!, startPos: playerStartPos ?? 0)
}
private func playmedia(urlInfo: VideoPlayURLInfo, playerInfo: PlayerInfo?) async {
let playURL = URL(string: BilibiliVideoResourceLoaderDelegate.URLs.play)!
let headers: [String: String] = [
"User-Agent": Keys.userAgent,
"Referer": "https://www.bilibili.com/video/av\(playInfo.aid)",
]
let asset = AVURLAsset(url: playURL, options: ["AVURLAssetHTTPHeaderFieldsKey": headers])
playerDelegate = BilibiliVideoResourceLoaderDelegate()
playerDelegate?.setBilibili(info: urlInfo, subtitles: playerInfo?.subtitle?.subtitles ?? [], aid: playInfo.aid)
if Settings.contentMatchOnlyInHDR {
if playerDelegate?.isHDR != true {
appliesPreferredDisplayCriteriaAutomatically = false
}
}
asset.resourceLoader.setDelegate(playerDelegate, queue: DispatchQueue(label: "loader"))
let requestedKeys = ["playable"]
await asset.loadValues(forKeys: requestedKeys)
prepare(toPlay: asset, withKeys: requestedKeys)
updatePlayerCharpter(playerInfo: playerInfo)
BiliBiliUpnpDMR.shared.sendVideoSwitch(aid: playInfo.aid, cid: playInfo.cid ?? 0)
}
private func updatePlayerCharpter(playerInfo: PlayerInfo?) {
let group = DispatchGroup()
var metas = [AVTimedMetadataGroup]()
for viewPoint in playerInfo?.view_points ?? [] {
group.enter()
convertTimedMetadataGroup(viewPoint: viewPoint) {
metas.append($0)
group.leave()
}
}
group.notify(queue: .main) {
if metas.count > 0 {
self.playerItem?.navigationMarkerGroups = [AVNavigationMarkersGroup(title: nil, timedNavigationMarkers: metas)]
}
}
}
override func extraInfoForPlayerError() -> String {
return playerDelegate?.infoDebugText ?? "-"
}
override func additionDebugInfo() -> String {
if let port = playerDelegate?.httpPort.string() {
return " :" + port
}
return ""
}
override func playerStatusDidChange() {
super.playerStatusDidChange()
switch player?.status {
case .readyToPlay:
BiliBiliUpnpDMR.shared.sendStatus(status: .playing)
case .failed:
BiliBiliUpnpDMR.shared.sendStatus(status: .stop)
default:
break
}
}
// override func playerRateDidChange(player: AVPlayer) {
// if player.rate > 0, danMuView.status == .pause {
// danMuView.play()
// } else if player.rate == 0, danMuView.status == .play {
// danMuView.pause()
// }
// }
func playNext() -> Bool {
if let next = nextProvider?.getNext() {
playInfo = next
playerStartPos = 0
Task {
await initPlayer()
}
return true
}
return false
}
override func playDidEnd() {
BiliBiliUpnpDMR.shared.sendStatus(status: .end)
if !playNext() {
if Settings.loopPlay {
nextProvider?.reset()
if !playNext() {
playerItem?.seek(to: .zero, completionHandler: nil)
player?.play()
}
return
}
dismiss(animated: true)
}
}
private func convertTimedMetadataGroup(viewPoint: PlayerInfo.ViewPoint, onResult: ((AVTimedMetadataGroup) -> Void)? = nil) {
let mapping: [AVMetadataIdentifier: Any?] = [
.commonIdentifierTitle: viewPoint.content,
]
var metadatas = mapping.compactMap { createMetadataItem(for: $0, value: $1) }
let timescale: Int32 = 600
let cmStartTime = CMTimeMakeWithSeconds(viewPoint.from, preferredTimescale: timescale)
let cmEndTime = CMTimeMakeWithSeconds(viewPoint.to, preferredTimescale: timescale)
let timeRange = CMTimeRangeFromTimeToTime(start: cmStartTime, end: cmEndTime)
if let pic = viewPoint.imgUrl?.addSchemeIfNeed() {
let resource = Kingfisher.ImageResource(downloadURL: pic)
KingfisherManager.shared.retrieveImage(with: resource) {
[weak self] result in
guard let self = self,
let data = try? result.get().image.pngData(),
let item = self.createMetadataItem(for: .commonIdentifierArtwork, value: data)
else {
onResult?(AVTimedMetadataGroup(items: metadatas, timeRange: timeRange))
return
}
metadatas.append(item)
onResult?(AVTimedMetadataGroup(items: metadatas, timeRange: timeRange))
}
} else {
onResult?(AVTimedMetadataGroup(items: metadatas, timeRange: timeRange))
}
}
}
// MARK: - Requests
extension VideoPlayerViewController {
func fetchVideoData() async {
assert(playInfo.isCidVaild)
let aid = playInfo.aid
let cid = playInfo.cid!
let info = try? await WebRequest.requestPlayerInfo(aid: aid, cid: cid)
do {
let playData: VideoPlayURLInfo
if playInfo.isBangumi {
playData = try await WebRequest.requestPcgPlayUrl(aid: aid, cid: cid)
clipInfos = playData.clip_info_list
} else {
playData = try await WebRequest.requestPlayUrl(aid: aid, cid: cid)
}
if info?.last_play_cid == cid, let startTime = info?.playTimeInSecond, playData.dash.duration - startTime > 5, Settings.continuePlay {
playerStartPos = startTime
}
await playmedia(urlInfo: playData, playerInfo: info)
if Settings.danmuMask {
if let mask = info?.dm_mask,
let video = playData.dash.video.first,
let fps = info?.dm_mask?.fps, fps > 0
{
maskProvider = BMaskProvider(info: mask, videoSize: CGSize(width: video.width ?? 0, height: video.height ?? 0))
} else if Settings.vnMask {
maskProvider = VMaskProvider()
}
setupMask()
}
if data == nil {
data = try? await WebRequest.requestDetailVideo(aid: aid)
}
setPlayerInfo(title: data?.title, subTitle: data?.ownerName, desp: data?.View.desc, pic: data?.pic)
} catch let err {
if case let .statusFail(code, message) = err as? RequestError {
if code == -404 || code == -10403 {
//
do {
if let ok = try await fetchAreaLimitVideoData(), ok {
return
}
} catch let err {
showErrorAlertAndExit(message: "请求失败,\(err)")
}
}
showErrorAlertAndExit(message: "请求失败\(code) \(message),可能需要大会员")
} else if info?.is_upower_exclusive == true {
showErrorAlertAndExit(message: "请求失败,该视频为充电专属视频 \(err)")
} else {
showErrorAlertAndExit(message: "请求失败,\(err)")
}
}
}
func fetchAreaLimitVideoData() async throws -> Bool? {
guard Settings.areaLimitUnlock else { return false }
guard let epid = playInfo.epid, epid > 0 else { return false }
let aid = playInfo.aid
let cid = playInfo.cid!
let season = try await WebRequest.requestBangumiSeasonView(epid: epid)
let checkTitle = season.title.contains("") ? season.title : season.series_title
let checkAreaList = parseAreaByTitle(title: checkTitle)
guard !checkAreaList.isEmpty else { return false }
let playData = try await requestAreaLimitPcgPlayUrl(epid: epid, cid: cid, areaList: checkAreaList)
guard let playData = playData else { return false }
let info = try? await WebRequest.requestPlayerInfo(aid: aid, cid: cid)
if info?.last_play_cid == cid, let startTime = info?.playTimeInSecond, playData.dash.duration - startTime > 5, Settings.continuePlay {
playerStartPos = startTime
} else {
playerStartPos = 0
}
await playmedia(urlInfo: playData, playerInfo: info)
if Settings.danmuMask {
if let mask = info?.dm_mask,
let video = playData.dash.video.first,
let fps = info?.dm_mask?.fps, fps > 0
{
maskProvider = BMaskProvider(info: mask, videoSize: CGSize(width: video.width ?? 0, height: video.height ?? 0))
} else if Settings.vnMask {
maskProvider = VMaskProvider()
}
setupMask()
}
if data == nil {
if let epi = season.episodes.first(where: { $0.ep_id == epid }) {
setPlayerInfo(title: epi.index + " " + (epi.index_title ?? ""), subTitle: season.up_info.uname, desp: season.evaluate, pic: epi.cover)
}
} else {
setPlayerInfo(title: data?.title, subTitle: data?.ownerName, desp: data?.View.desc, pic: data?.pic)
}
return true
}
private func requestAreaLimitPcgPlayUrl(epid: Int, cid: Int, areaList: [String]) async throws -> VideoPlayURLInfo? {
for area in areaList {
do {
return try await WebRequest.requestAreaLimitPcgPlayUrl(epid: epid, cid: cid, area: area)
} catch let err {
if area == areaList.last {
throw err
} else {
print(err)
}
}
}
return nil
}
private func parseAreaByTitle(title: String) -> [String] {
if title.isMatch(pattern: "[仅|僅].*[东南亚|其他]") {
// TODO:
return []
}
var areas: [String] = []
if title.isMatch(pattern: "僅.*台") {
areas.append("tw")
}
if title.isMatch(pattern: "僅.*港") {
areas.append("hk")
}
if areas.isEmpty {
//
return ["tw", "hk"]
} else {
return areas
}
}
}
// MARK: - Player
extension VideoPlayerViewController {
@MainActor
func prepare(toPlay asset: AVURLAsset, withKeys requestedKeys: [AnyHashable]) {
for thisKey in requestedKeys {
guard let thisKey = thisKey as? String else {
continue
}
var error: NSError?
let keyStatus = asset.statusOfValue(forKey: thisKey, error: &error)
if keyStatus == .failed {
showErrorAlertAndExit(title: error?.localizedDescription ?? "", message: error?.localizedFailureReason ?? "")
return
}
}
if !asset.isPlayable {
showErrorAlertAndExit(message: "URL解析错误")
return
}
playerItem = AVPlayerItem(asset: asset)
let player = AVPlayer(playerItem: playerItem)
player.addPeriodicTimeObserver(forInterval: CMTime(seconds: 1, preferredTimescale: 1), queue: .main) { [weak self] time in
guard let self else { return }
if self.danMuView.isHidden { return }
let seconds = time.seconds
self.danmuProvider.playerTimeChange(time: seconds)
if let duration = self.data?.View.duration {
BiliBiliUpnpDMR.shared.sendProgress(duration: duration, current: Int(seconds))
}
if let clipInfos = self.clipInfos {
var matched = false
for clip in clipInfos {
if seconds > clip.start, seconds < clip.end {
let action = {
clip.skipped = true
self.player?.seek(to: CMTime(seconds: Double(clip.end), preferredTimescale: 1), toleranceBefore: .zero, toleranceAfter: .zero)
}
if !(clip.skipped ?? false), Settings.autoSkip {
action()
self.skipAction = nil
} else if self.skipAction?.accessibilityLabel != clip.a11Tag {
self.skipAction = UIAction(title: clip.customText) { _ in
action()
}
self.skipAction?.accessibilityLabel = clip.a11Tag
}
self.contextualActions = [self.skipAction].compactMap { $0 }
matched = true
break
}
}
if !matched {
self.contextualActions = []
}
}
}
if let defaultRate = self.player?.defaultRate,
let speed = PlaySpeed.blDefaults.first(where: { $0.value == defaultRate })
{
self.player = player
selectSpeed(AVPlaybackSpeed(rate: speed.value, localizedName: speed.name))
} else {
self.player = player
await viewModel.load()
}
}
}

View File

@ -0,0 +1,183 @@
//
// NewVideoPlayerViewModel.swift
// BilibiliLive
//
// Created by yicheng on 2024/5/23.
//
import Combine
import UIKit
struct PlayerDetailData {
let aid: Int
let cid: Int
let epid: Int? //
let isBangumi: Bool
var playerStartPos: Int?
var detail: VideoDetail?
var clips: [VideoPlayURLInfo.ClipInfo]?
var playerInfo: PlayerInfo?
var videoPlayURLInfo: VideoPlayURLInfo
}
class VideoPlayerViewModel {
var onPluginReady = PassthroughSubject<[CommonPlayerPlugin], String>()
var onPluginRemove = PassthroughSubject<CommonPlayerPlugin, Never>()
var onExit: (() -> Void)?
var nextProvider: VideoNextProvider?
private var playInfo: PlayInfo
private let danmuProvider = VideoDanmuProvider()
private var videoDetail: VideoDetail?
private var cancellable = Set<AnyCancellable>()
private var playPlugin: CommonPlayerPlugin?
init(playInfo: PlayInfo) {
self.playInfo = playInfo
}
func load() async {
do {
let data = try await loadVideoInfo()
let plugin = await generatePlayerPlugin(data)
onPluginReady.send(plugin)
} catch let err {
onPluginReady.send(completion: .failure(err.localizedDescription))
}
}
private func loadVideoInfo() async throws -> PlayerDetailData {
try await initPlayInfo()
let data = try await fetchVideoData()
await danmuProvider.initVideo(cid: data.cid, startPos: data.playerStartPos ?? 0)
return data
}
private func initPlayInfo() async throws {
if !playInfo.isCidVaild {
playInfo.cid = try await WebRequest.requestCid(aid: playInfo.aid)
}
BiliBiliUpnpDMR.shared.sendVideoSwitch(aid: playInfo.aid, cid: playInfo.cid ?? 0)
}
private func updateVideoDetailIfNeeded() async {
if videoDetail == nil {
videoDetail = try? await WebRequest.requestDetailVideo(aid: playInfo.aid)
}
}
private func fetchVideoData() async throws -> PlayerDetailData {
assert(playInfo.isCidVaild)
let aid = playInfo.aid
let cid = playInfo.cid!
async let infoReq = try? WebRequest.requestPlayerInfo(aid: aid, cid: cid)
async let detailUpdate: () = updateVideoDetailIfNeeded()
do {
let playData: VideoPlayURLInfo
var clipInfos: [VideoPlayURLInfo.ClipInfo]?
if playInfo.isBangumi {
playData = try await WebRequest.requestPcgPlayUrl(aid: aid, cid: cid)
clipInfos = playData.clip_info_list
} else {
playData = try await WebRequest.requestPlayUrl(aid: aid, cid: cid)
}
let info = await infoReq
_ = await detailUpdate
var detail = PlayerDetailData(aid: playInfo.aid, cid: playInfo.cid!, epid: playInfo.epid, isBangumi: playInfo.isBangumi, detail: videoDetail, clips: clipInfos, playerInfo: info, videoPlayURLInfo: playData)
if let info, info.last_play_cid == cid, playData.dash.duration - info.playTimeInSecond > 5, Settings.continuePlay {
detail.playerStartPos = info.playTimeInSecond
}
return detail
} catch let err {
if case let .statusFail(code, message) = err as? RequestError {
if code == -404 || code == -10403 {
//
// do {
// if let ok = try await fetchAreaLimitVideoData(), ok {
// return
// }
// } catch let err {
// }
}
throw "\(code) \(message),可能需要大会员"
} else if await infoReq?.is_upower_exclusive == true {
throw "该视频为充电专属视频 \(err)"
} else {
throw err
}
}
}
private func playNext(newPlayInfo: PlayInfo) {
playInfo = newPlayInfo
if let playPlugin {
onPluginRemove.send(playPlugin)
}
Task {
do {
let data = try await loadVideoInfo()
let player = BVideoPlayPlugin(detailData: data)
onPluginReady.send([player])
} catch let err {
onPluginReady.send(completion: .failure(err.localizedDescription))
}
}
}
@MainActor private func generatePlayerPlugin(_ data: PlayerDetailData) async -> [CommonPlayerPlugin] {
let player = BVideoPlayPlugin(detailData: data)
let danmu = DanmuViewPlugin(provider: danmuProvider)
let upnp = BUpnpPlugin(duration: data.detail?.View.duration)
let debug = DebugPlugin()
let playSpeed = SpeedChangerPlugin()
playSpeed.$currentPlaySpeed.sink { [weak danmu] speed in
danmu?.danMuView.playingSpeed = speed.value
}.store(in: &cancellable)
let playlist = VideoPlayListPlugin(nextProvider: nextProvider)
playlist.onPlayEnd = { [weak self] in
self?.onExit?()
}
playlist.onPlayNextWithInfo = {
[weak self] info in
guard let self else { return }
playNext(newPlayInfo: info)
}
playPlugin = player
var plugins: [CommonPlayerPlugin] = [player, danmu, playSpeed, upnp, debug, playlist]
if let clips = data.clips {
let clip = BVideoClipsPlugin(clipInfos: clips)
plugins.append(clip)
}
if Settings.danmuMask {
if let mask = data.playerInfo?.dm_mask,
let video = data.videoPlayURLInfo.dash.video.first,
mask.fps > 0
{
let maskProvider = BMaskProvider(info: mask, videoSize: CGSize(width: video.width ?? 0, height: video.height ?? 0))
plugins.append(MaskViewPugin(maskView: danmu.danMuView, maskProvider: maskProvider))
} else if Settings.vnMask {
let maskProvider = VMaskProvider()
plugins.append(MaskViewPugin(maskView: danmu.danMuView, maskProvider: maskProvider))
}
}
if let detail = data.detail {
let info = BVideoInfoPlugin(title: detail.title, subTitle: detail.ownerName, desp: detail.View.desc, pic: detail.pic, viewPoints: data.playerInfo?.view_points)
plugins.append(info)
}
return plugins
}
}

View File

@ -0,0 +1,29 @@
//
// Created by Yam on 2024/6/9.
//
import Kingfisher
import UIKit
class ReplyCell: UICollectionViewCell {
class var identifier: String {
return String(describing: Self.self)
}
@IBOutlet var avatarImageView: UIImageView!
@IBOutlet var userNameLabel: UILabel!
@IBOutlet var contenLabel: UILabel!
func config(replay: Replys.Reply) {
avatarImageView.kf.setImage(
with: URL(string: replay.member.avatar),
options: [
.processor(DownsamplingImageProcessor(size: CGSize(width: 80, height: 80))),
.processor(RoundCornerImageProcessor(radius: .widthFraction(0.5))),
.cacheSerializer(FormatIndicatedCacheSerializer.png),
]
)
userNameLabel.text = replay.member.uname
contenLabel.text = replay.content.message
}
}

View File

@ -0,0 +1,82 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder.AppleTV.XIB" version="3.0" toolsVersion="32700.99.1234" targetRuntime="AppleTV" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="appleTV" appearance="light"/>
<dependencies>
<deployment identifier="tvOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22613"/>
<capability name="TVUIKit controls" minToolsVersion="10.2"/>
<capability name="collection view cell content view" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<collectionViewCell opaque="NO" multipleTouchEnabled="YES" contentMode="center" reuseIdentifier="ReplyCell" id="Eh2-Du-lue" customClass="ReplyCell" customModule="BilibiliLive" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="582" height="360"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<collectionViewCellContentView key="contentView" opaque="NO" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="Wff-Lz-AFu">
<rect key="frame" x="0.0" y="0.0" width="582" height="360"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<tvCardView contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" translatesAutoresizingMaskIntoConstraints="NO" id="BbK-et-U1E" customClass="BLCardView" customModule="BilibiliLive" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="582" height="360"/>
<tvCardContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="NU3-Bt-p2n">
<rect key="frame" x="23" y="23" width="536" height="314"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" tag="1" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="QAe-nh-BI1">
<rect key="frame" x="20" y="20" width="50" height="50"/>
<constraints>
<constraint firstAttribute="height" constant="50" id="igG-8K-FYQ"/>
<constraint firstAttribute="width" constant="50" id="y0q-EX-6LB"/>
</constraints>
</imageView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="cNI-2h-EqQ">
<rect key="frame" x="78" y="27.5" width="71" height="35"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="rG1-i7-Lu2">
<rect key="frame" x="20" y="70" width="496" height="244"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<constraints>
<constraint firstItem="cNI-2h-EqQ" firstAttribute="leading" secondItem="QAe-nh-BI1" secondAttribute="trailing" constant="8" symbolic="YES" id="4dP-d4-GdT"/>
<constraint firstAttribute="trailing" secondItem="rG1-i7-Lu2" secondAttribute="trailing" constant="20" id="GSC-ii-sg3"/>
<constraint firstAttribute="bottom" secondItem="rG1-i7-Lu2" secondAttribute="bottom" id="Y41-Mq-PTg"/>
<constraint firstItem="cNI-2h-EqQ" firstAttribute="centerY" secondItem="QAe-nh-BI1" secondAttribute="centerY" id="eDT-iC-jop"/>
<constraint firstItem="rG1-i7-Lu2" firstAttribute="leading" secondItem="NU3-Bt-p2n" secondAttribute="leading" constant="20" id="jyZ-sx-GoO"/>
<constraint firstItem="QAe-nh-BI1" firstAttribute="top" secondItem="NU3-Bt-p2n" secondAttribute="top" constant="20" id="nxD-2i-sMk"/>
<constraint firstItem="rG1-i7-Lu2" firstAttribute="top" secondItem="QAe-nh-BI1" secondAttribute="bottom" id="p2n-wK-8Mf"/>
<constraint firstItem="QAe-nh-BI1" firstAttribute="leading" secondItem="NU3-Bt-p2n" secondAttribute="leading" constant="20" id="q3B-Ru-Hbx"/>
</constraints>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="boolean" keyPath="layer.masksToBounds" value="YES"/>
<userDefinedRuntimeAttribute type="number" keyPath="layer.cornerRadius">
<integer key="value" value="12"/>
</userDefinedRuntimeAttribute>
</userDefinedRuntimeAttributes>
</tvCardContentView>
</tvCardView>
</subviews>
<constraints>
<constraint firstItem="BbK-et-U1E" firstAttribute="top" secondItem="Wff-Lz-AFu" secondAttribute="top" id="Jbi-rF-EEN"/>
<constraint firstAttribute="bottom" secondItem="BbK-et-U1E" secondAttribute="bottom" id="SIX-9P-hvK"/>
<constraint firstAttribute="trailing" secondItem="BbK-et-U1E" secondAttribute="trailing" id="bBd-7k-i5K"/>
<constraint firstItem="BbK-et-U1E" firstAttribute="leading" secondItem="Wff-Lz-AFu" secondAttribute="leading" id="fIO-WV-pqN"/>
</constraints>
</collectionViewCellContentView>
<size key="customSize" width="582" height="360"/>
<connections>
<outlet property="avatarImageView" destination="QAe-nh-BI1" id="1rg-6A-61M"/>
<outlet property="contenLabel" destination="rG1-i7-Lu2" id="jx0-u9-gWO"/>
<outlet property="userNameLabel" destination="cNI-2h-EqQ" id="IuH-sE-r7M"/>
</connections>
<point key="canvasLocation" x="-146" y="233"/>
</collectionViewCell>
</objects>
</document>

View File

@ -0,0 +1,20 @@
//
// Published+..swift
// BilibiliLive
//
// Created by yicheng on 2024/6/10.
//
import Combine
fileprivate var cancellables = [String: AnyCancellable]()
public extension Published {
init(wrappedValue defaultValue: Value, key: String) {
let value = UserDefaults.standard.object(forKey: key) as? Value ?? defaultValue
self.init(initialValue: value)
cancellables[key] = projectedValue.sink { val in
UserDefaults.standard.set(val, forKey: key)
}
}
}

View File

@ -14,6 +14,9 @@ import UIKit
class BiliBiliUpnpDMR: NSObject {
static let shared = BiliBiliUpnpDMR()
weak var currentPlugin: BUpnpPlugin?
private var udp: GCDAsyncUdpSocket!
private var httpServer = HttpServer()
private var connectedSockets = [GCDAsyncSocket]()
@ -221,18 +224,18 @@ class BiliBiliUpnpDMR: NSObject {
handlePlay(json: JSON(parseJSON: frame.body))
session.sendEmpty()
case "Pause":
(topMost as? CommonPlayerViewController)?.player?.pause()
currentPlugin?.pause()
session.sendEmpty()
case "Resume":
(topMost as? CommonPlayerViewController)?.player?.play()
currentPlugin?.resume()
session.sendEmpty()
case "SwitchDanmaku":
let json = JSON(parseJSON: frame.body)
(topMost as? CommonPlayerViewController)?.danMuView.isHidden = !json["open"].boolValue
Defaults.shared.showDanmu = json["open"].boolValue
session.sendEmpty()
case "Seek":
let json = JSON(parseJSON: frame.body)
(topMost as? VideoPlayerViewController)?.player?.seek(to: CMTime(seconds: json["seekTs"].doubleValue, preferredTimescale: 1), toleranceBefore: .zero, toleranceAfter: .zero)
currentPlugin?.seek(to: json["seekTs"].doubleValue)
session.sendEmpty()
case "Stop":
(topMost as? CommonPlayerViewController)?.dismiss(animated: true)

View File

@ -11,7 +11,7 @@ import Foundation
import SwiftyJSON
import UIKit
class LivePlayerViewController: NewCommonPlayerViewController {
class LivePlayerViewController: CommonPlayerViewController {
var room: LiveRoom?
private var viewModel: LivePlayerViewModel?

View File

@ -707,6 +707,7 @@ struct Replys: Codable, Hashable {
let member: Member
let content: Content
let replies: [Reply]?
}
let replies: [Reply]?

View File

@ -168,7 +168,8 @@ GEM
trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0)
retriable (3.1.2)
rexml (3.2.5)
rexml (3.2.9)
strscan
rouge (2.0.7)
ruby2_keywords (0.0.5)
rubyzip (2.3.2)
@ -181,6 +182,7 @@ GEM
simctl (1.6.8)
CFPropertyList
naturally
strscan (3.1.0)
terminal-notifier (2.0.0)
terminal-table (1.8.0)
unicode-display_width (~> 1.1, >= 1.1.1)
@ -210,6 +212,7 @@ GEM
PLATFORMS
arm64-darwin-21
x86_64-linux
DEPENDENCIES
fastlane